1
#![allow(dead_code)]
2
use crate::ast::enumerated::EnumVariant;
3
use crate::ast::{
4
    AbstractLiteral, Atom, DeclarationKind, Expression as Expr, Field, Literal as Lit, Metadata,
5
    Moo,
6
    comprehension::{Comprehension, ComprehensionQualifier},
7
    matrix,
8
};
9
use crate::bug_assert;
10
use crate::into_matrix;
11
use itertools::{Itertools as _, izip};
12
use std::cmp::Ordering as CmpOrdering;
13
use std::collections::HashSet;
14

            
15
use super::partial_eval::{run_partial_evaluator, run_partial_evaluator_local};
16

            
17
3436
pub(crate) fn factorial_i32(n: i32) -> Option<i32> {
18
3436
    if n < 0 {
19
        return None;
20
3436
    }
21

            
22
3436
    (1..=n).try_fold(1_i32, i32::checked_mul)
23
3436
}
24

            
25
fn eval_constant_set(expr: &Expr) -> Option<Vec<Lit>> {
26
    let Lit::AbstractLiteral(AbstractLiteral::Set(values)) = eval_constant(expr)? else {
27
        return None;
28
    };
29

            
30
    Some(values)
31
}
32

            
33
/// Compare literals only when their outer Essence literal kinds agree.
34
///
35
/// Inner types are guaranteed by expression type checking; keeping this guard here also makes
36
/// direct evaluator calls on unlike primitive or abstract literal kinds safely unevaluable.
37
10140294
fn equal_constant_literals(lhs: &Lit, rhs: &Lit) -> Option<bool> {
38
10140294
    match (lhs, rhs) {
39
12
        (Lit::EnumVariant(lhs), Lit::EnumVariant(rhs)) if lhs.ty == rhs.ty => {
40
12
            Some(lhs.variant == rhs.variant)
41
        }
42
8
        (Lit::Unnamed(lhs), Lit::Unnamed(rhs)) if lhs.ty == rhs.ty => Some(lhs.value == rhs.value),
43
        (Lit::Int(_), Lit::Int(_)) | (Lit::Bool(_), Lit::Bool(_)) => {
44
9813847
            Some(lhs.essence_cmp(rhs) == CmpOrdering::Equal)
45
        }
46
        (Lit::AbstractLiteral(lhs_abstract), Lit::AbstractLiteral(rhs_abstract))
47
326369
            if std::mem::discriminant(lhs_abstract) == std::mem::discriminant(rhs_abstract) =>
48
        {
49
326369
            Some(lhs.essence_cmp(rhs) == CmpOrdering::Equal)
50
        }
51
58
        _ => None,
52
    }
53
10140294
}
54

            
55
/// Preserve already-distinct literal order; constant duplicates denote the same set member.
56
572073028
fn normalise_constant_set(mut literal: Lit) -> Lit {
57
572073028
    if let Lit::AbstractLiteral(AbstractLiteral::Set(items)) = &mut literal
58
3570878
        && super::literals::distinct_literal_count(items) != items.len()
59
    {
60
39907
        items.sort_by(Lit::essence_cmp);
61
134302
        items.dedup_by(|a, b| a.essence_cmp(b).is_eq());
62
572033121
    }
63
572073028
    literal
64
572073028
}
65

            
66
/// Simplify an expression to a constant using only constants already present at this node.
67
///
68
/// This is intended for the rewriter: child expressions should have been simplified by the
69
/// scheduler before their parent is considered. Use [`eval_constant`] when a caller explicitly
70
/// wants recursive evaluation of an arbitrary expression.
71
1641994121
pub fn eval_constant_local(expr: &Expr) -> Option<Lit> {
72
1641994121
    if !has_only_locally_evaluable_operands(expr) {
73
489088160
        return None;
74
1152905961
    }
75

            
76
1152905961
    eval_constant(expr)
77
1641994121
}
78

            
79
/// Applies the evaluator normalisation hook to a focused expression.
80
///
81
/// Evaluators are privileged simplifications, not ordinary rewrite rules. The rewriter invokes
82
/// this hook before normal rule scheduling and immediately after a successful ordinary rule,
83
/// walking upward while evaluation keeps simplifying parents. This exploits the semantic property
84
/// that local constant and partial evaluation is always preferable to trying lower-priority rules,
85
/// while avoiding millions of failed universal `constant_evaluator` rule attempts.
86
///
87
/// Away from [`Expr::Root`], the hook is pure and local: it does not create auxiliaries, mutate
88
/// the symbol table, or recursively inspect arbitrary descendants. Children are expected to have
89
/// been normalised by the scheduler before their parent is evaluated.
90
///
91
/// At [`Expr::Root`], a selective deep pass runs over top-level constraints (skipping solver-flat
92
/// forms). Callers that know which root child changed should prefer
93
/// [`normalise_root_selective_deep_expr`] with `only_constraint` so sibling constraints are not
94
/// re-traversed. Local root-list reshaping (flatten top-level `and`) is intentionally deferred to
95
/// [`finish_root_evaluator_normalisation`]: doing it mid-loop materialises huge root lists and
96
/// explodes worklist rule attempts.
97
1679842111
pub fn normalise_evaluator_local(expr: &Expr) -> Option<Expr> {
98
1679842111
    match expr {
99
18157650
        Expr::Root(_, exprs) => normalise_root_constraints_selective_deep(exprs, None),
100
        // Focused `AbstractLiteral` literals must not repeatedly refold to themselves; parents
101
        // still see the `Atomic(Literal(...))` form when the hook walks upward.
102
14752959
        Expr::Atomic(_, Atom::Literal(Lit::AbstractLiteral(_))) => None,
103
1646931502
        _ => fold_constant_expression_local(expr).or_else(|| {
104
1637353590
            run_partial_evaluator_local(expr)
105
1637353590
                .ok()
106
5906254
                .map(|reduction| reduction.new_expression)
107
1637353590
        }),
108
    }
109
16430823
    .filter(|new_expr| new_expr != expr)
110
1679842111
}
111

            
112
/// Applies local root-list partial evaluation (strip `true`, propagate `false`, flatten `and`s).
113
1643252
pub fn normalise_root_constraints_local(exprs: &[Expr]) -> Option<Expr> {
114
1643252
    if exprs.is_empty() {
115
        return Some(Expr::Root(Metadata::new(), vec![true.into()]));
116
1643252
    }
117

            
118
1643252
    let root = Expr::Root(Metadata::new(), exprs.to_vec());
119
1643252
    run_partial_evaluator_local(&root)
120
1643252
        .ok()
121
291711
        .map(|reduction| reduction.new_expression)
122
291711
        .filter(|new_root| new_root != &root)
123
1643252
}
124

            
125
/// Deep-normalises selected non-flat top-level constraints in `root`.
126
5
pub fn normalise_root_selective_deep_expr(
127
5
    root: &Expr,
128
5
    only_constraint: Option<usize>,
129
5
) -> Option<Expr> {
130
5
    let Expr::Root(_, exprs) = root else {
131
        return None;
132
    };
133

            
134
5
    normalise_root_constraints_selective_deep(exprs, only_constraint)
135
5
}
136

            
137
4
pub fn normalise_root_constraints_deep(root: &Expr) -> Option<Expr> {
138
4
    normalise_root_selective_deep_expr(root, None)
139
4
}
140

            
141
/// Finishes evaluator normalisation on the model root after rewriting completes.
142
///
143
/// Applies local root-list partial evaluation to a fixpoint (strip `true`, propagate `false`,
144
/// flatten top-level `and`). Deep evaluation of individual constraints already runs during
145
/// rewriting via [`normalise_evaluator_local`] / [`normalise_root_selective_deep_expr`]; this
146
/// finish pass must not be inlined into the mid-loop Root hook because flattening `and` early
147
/// explodes worklist size on large expansions.
148
1351541
pub fn finish_root_evaluator_normalisation(root: &Expr) -> Option<Expr> {
149
1351541
    let Expr::Root(_, exprs) = root else {
150
        return None;
151
    };
152

            
153
1351541
    let mut current = Expr::Root(Metadata::new(), exprs.clone());
154
1351541
    let mut changed = false;
155
1643250
    while let Expr::Root(_, current_exprs) = &current {
156
1643250
        let Some(next) = normalise_root_constraints_local(current_exprs) else {
157
1351541
            break;
158
        };
159
291709
        current = next;
160
291709
        changed = true;
161
    }
162
1351541
    changed.then_some(current)
163
1351541
}
164

            
165
/// Deep-normalises a single top-level constraint, or returns `None` if it is already normal.
166
///
167
/// Callers that know which root child changed should prefer this over rebuilding the whole root:
168
/// the root hook runs after every rewrite, so touching siblings there costs O(model size) per
169
/// rewrite.
170
183517137
pub fn normalise_root_constraint_deep(constraint: &Expr) -> Option<Expr> {
171
183517137
    if constraint_skips_deep_root_normalisation(constraint) {
172
181
        return None;
173
183516956
    }
174

            
175
183516956
    normalise_constraint_deep_to_fixpoint(constraint)
176
183517137
}
177

            
178
18157655
fn normalise_root_constraints_selective_deep(
179
18157655
    exprs: &[Expr],
180
18157655
    only_constraint: Option<usize>,
181
18157655
) -> Option<Expr> {
182
18157655
    if exprs.is_empty() {
183
921978
        return Some(Expr::Root(Metadata::new(), vec![true.into()]));
184
17235677
    }
185

            
186
    // Normalise first and only rebuild the root list once something has actually changed. Building
187
    // the replacement eagerly clones every sibling constraint on each call, which dominates the
188
    // post-rewrite root hook on models with many top-level constraints.
189
17235677
    let mut normalised: Vec<(usize, Expr)> = Vec::new();
190
17235677
    for (index, constraint) in exprs.iter().enumerate() {
191
183516698
        if only_constraint.is_some_and(|only| only != index) {
192
1
            continue;
193
183516697
        }
194

            
195
183516697
        if let Some(replacement) = normalise_root_constraint_deep(constraint) {
196
36862
            normalised.push((index, replacement));
197
183479835
        }
198
    }
199

            
200
17235677
    if normalised.is_empty() {
201
17210995
        return None;
202
24682
    }
203

            
204
24682
    let mut constraints = exprs.to_vec();
205
24682
    for (index, replacement) in normalised {
206
36862
        constraints[index] = replacement;
207
    }
208

            
209
24682
    Some(Expr::Root(Metadata::new(), constraints))
210
18157655
}
211

            
212
/// Whether a top-level constraint has already been lowered to solver-flat form.
213
///
214
/// Deep root normalisation must not rewrite these: doing so can disturb auxiliaries introduced
215
/// for Minion (for example chained `FlatProductEq` constraints) or repeat expensive work.
216
183517137
fn constraint_skips_deep_root_normalisation(expr: &Expr) -> bool {
217
183517137
    match expr {
218
        Expr::FlatProductEq(_, _, _, _)
219
        | Expr::FlatSumLeq(_, _, _)
220
        | Expr::FlatSumGeq(_, _, _)
221
        | Expr::FlatMinEq(_, _, _)
222
        | Expr::FlatIneq(_, _, _, _)
223
        | Expr::FlatMinusEq(_, _, _)
224
        | Expr::FlatAbsEq(_, _, _)
225
        | Expr::FlatAllDiff(_, _)
226
        | Expr::FlatTable(..)
227
        | Expr::FlatShortTable(..)
228
        | Expr::SmtDistinct(_, _)
229
        | Expr::FlatWeightedSumLeq(_, _, _, _)
230
        | Expr::FlatWeightedSumGeq(_, _, _, _)
231
        | Expr::FlatWatchedLiteral(_, _, _)
232
        | Expr::MinionDivEqUndefZero(_, _, _, _)
233
        | Expr::MinionModuloEqUndefZero(_, _, _, _)
234
        | Expr::MinionPow(_, _, _, _)
235
        | Expr::MinionReify(_, _, _)
236
        | Expr::MinionReifyImply(_, _, _)
237
        | Expr::MinionWInIntervalSet(_, _, _)
238
        | Expr::MinionWInSet(_, _, _)
239
181
        | Expr::MinionElementOne(_, _, _, _) => true,
240
6267915
        Expr::AuxDeclaration(_, _, inner) => matches!(
241
6267915
            inner.as_ref(),
242
            Expr::Product(_, _) | Expr::FlatProductEq(_, _, _, _)
243
        ),
244
177249041
        _ => false,
245
    }
246
183517137
}
247

            
248
144531023
fn fold_constant_expression_deep(expr: &Expr) -> Option<Expr> {
249
144531023
    let constant = eval_constant(expr)?;
250
36863
    fold_constant_expression(expr, constant).filter(|folded| folded != expr)
251
144531023
}
252

            
253
183516956
fn normalise_constraint_deep_to_fixpoint(constraint: &Expr) -> Option<Expr> {
254
183516956
    if matches!(constraint, Expr::Atomic(_, Atom::Literal(_))) {
255
39022795
        return None;
256
144494161
    }
257

            
258
144494161
    let mut current = constraint.clone();
259
144494161
    let mut changed = false;
260

            
261
144531023
    while let Some(step) = fold_constant_expression_deep(&current)
262
144531021
        .or_else(|| partial_evaluator_deep_step(&current))
263
36862
        .filter(|step| step != &current)
264
    {
265
36862
        current = step;
266
36862
        changed = true;
267
    }
268

            
269
144494161
    changed
270
144494161
        .then_some(current)
271
36862
        .filter(|current| current != constraint)
272
183516956
}
273

            
274
144531021
fn partial_evaluator_deep_step(expr: &Expr) -> Option<Expr> {
275
144531021
    run_partial_evaluator(expr)
276
144531021
        .ok()
277
36860
        .map(|reduction| reduction.new_expression)
278
36860
        .filter(|new_expr| new_expr != expr)
279
144531021
}
280

            
281
/// Constant-folds `expr` locally unless doing so would inline a referenced matrix literal.
282
1646931502
fn fold_constant_expression_local(expr: &Expr) -> Option<Expr> {
283
1646931502
    let constant = match expr {
284
        // Comprehensions are atomic in arena traversal; evaluate them in one step here rather than
285
        // via operand checks that would repeat the same work for every parent.
286
4937381
        Expr::Comprehension(_, _) => eval_constant(expr)?,
287
1641994121
        _ => eval_constant_local(expr)?,
288
    };
289
199605273
    fold_constant_expression(expr, constant).filter(|folded| folded != expr)
290
1646931502
}
291

            
292
199642136
fn fold_constant_expression(expr: &Expr, constant: Lit) -> Option<Expr> {
293
199642136
    if let Expr::Atomic(_, Atom::Literal(existing)) = expr
294
189955729
        && existing == &constant
295
    {
296
189955729
        return None;
297
9686407
    }
298

            
299
9686407
    if matches!(
300
9686407
        (expr, constant.clone()),
301
        (
302
            Expr::Atomic(_, Atom::Reference(_)),
303
            Lit::AbstractLiteral(AbstractLiteral::Matrix(_, _))
304
        )
305
    ) {
306
93670
        return None;
307
9592737
    }
308

            
309
9592737
    let folded = Expr::Atomic(Metadata::new(), Atom::Literal(constant));
310
9592737
    if let Expr::TypeAnnotation(_, _, domain) = expr
311
8555
        && let Expr::Atomic(
312
8555
            _,
313
8555
            Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Matrix(elems, _))),
314
8555
        ) = &folded
315
8555
        && elems.is_empty()
316
    {
317
8555
        return Some(Expr::TypeAnnotation(
318
8555
            Metadata::new(),
319
8555
            Moo::new(folded),
320
8555
            domain.clone(),
321
8555
        ));
322
9584182
    }
323

            
324
9584182
    if let Expr::DomainAnnotation(_, _, domain) = expr
325
6268
        && let Expr::Atomic(
326
6268
            _,
327
6268
            Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Matrix(elems, _))),
328
6268
        ) = &folded
329
6268
        && elems.is_empty()
330
    {
331
6268
        return Some(Expr::DomainAnnotation(
332
6268
            Metadata::new(),
333
6268
            Moo::new(folded),
334
6268
            domain.clone(),
335
6268
        ));
336
9577914
    }
337

            
338
9577914
    if let Expr::Comprehension(_, comprehension) = expr
339
35613
        && let Expr::Atomic(
340
35613
            _,
341
35613
            Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Matrix(elems, _))),
342
35613
        ) = &folded
343
35613
        && elems.is_empty()
344
6268
        && let Some(domain) = comprehension.domain_of()
345
    {
346
6268
        return Some(Expr::DomainAnnotation(
347
6268
            Metadata::new(),
348
6268
            Moo::new(folded),
349
6268
            domain,
350
6268
        ));
351
9571646
    }
352

            
353
9571646
    Some(folded)
354
199642136
}
355

            
356
1641994121
fn has_only_locally_evaluable_operands(expr: &Expr) -> bool {
357
1641994121
    match expr {
358
189918868
        Expr::Atomic(_, Atom::Literal(_)) => true,
359
        // Whether a reference resolves to a constant is the evaluator's result, not an
360
        // applicability question. Resolving it here would clone/evaluate the value letting and
361
        // then make eval_constant do the same work again.
362
582450297
        Expr::Atomic(_, Atom::Reference(_)) => true,
363
256896316
        Expr::AbstractLiteral(_, lit) => abstract_literal_children_are_locally_evaluable(lit),
364
        Expr::TypeAnnotation(_, inner, _) | Expr::DomainAnnotation(_, inner, _) => {
365
14839
            is_locally_evaluable_expr(inner.as_ref())
366
        }
367
        Expr::Comprehension(_, _) | Expr::Root(_, _) => false,
368
        // Visit children by reference. `Uniplate::children()` clones the whole child list, so on a
369
        // wide node (e.g. `or` over a large matrix) this test alone costs O(subtree); the evaluator
370
        // hook runs it on every ancestor after every rewrite, which makes rewriting quadratic.
371
        _ => {
372
612713801
            let mut all_constant = true;
373
831188673
            expr.for_each_expr_child(&mut |child| {
374
831188673
                all_constant = all_constant && is_locally_evaluable_expr(child);
375
831188673
            });
376
612713801
            all_constant
377
        }
378
    }
379
1641994121
}
380

            
381
1714297238
fn is_locally_evaluable_expr(expr: &Expr) -> bool {
382
1714297238
    match expr {
383
123331943
        Expr::Atomic(_, Atom::Literal(_)) => true,
384
848793050
        Expr::Atomic(_, Atom::Reference(_)) => true,
385
253069246
        Expr::AbstractLiteral(_, lit) => abstract_literal_children_are_locally_evaluable(lit),
386
        Expr::TypeAnnotation(_, inner, _) | Expr::DomainAnnotation(_, inner, _) => {
387
14839
            is_locally_evaluable_expr(inner.as_ref())
388
        }
389
489088160
        _ => false,
390
    }
391
1714297238
}
392

            
393
509965562
fn abstract_literal_children_are_locally_evaluable(lit: &AbstractLiteral<Expr>) -> bool {
394
509965562
    match lit {
395
        AbstractLiteral::Set(items)
396
        | AbstractLiteral::MSet(items)
397
        | AbstractLiteral::Tuple(items)
398
504021432
        | AbstractLiteral::Matrix(items, _) => items.iter().all(is_locally_evaluable_expr),
399
18705
        AbstractLiteral::Record(fields) => fields
400
18705
            .iter()
401
37410
            .all(|field| is_locally_evaluable_expr(&field.value)),
402
5319238
        AbstractLiteral::Sequence(items) => items.iter().all(is_locally_evaluable_expr),
403
        AbstractLiteral::Function(items) => items
404
            .iter()
405
            .all(|(from, to)| is_locally_evaluable_expr(from) && is_locally_evaluable_expr(to)),
406
2117
        AbstractLiteral::Relation(items) => items
407
2117
            .iter()
408
6351
            .all(|tuple| tuple.iter().all(is_locally_evaluable_expr)),
409
522
        AbstractLiteral::Partition(parts) => parts
410
522
            .iter()
411
522
            .all(|part| part.iter().all(is_locally_evaluable_expr)),
412
16298
        AbstractLiteral::Permutation(cycles) => cycles
413
16298
            .iter()
414
16733
            .all(|cycle| cycle.iter().all(is_locally_evaluable_expr)),
415
587250
        AbstractLiteral::Variant(field) => is_locally_evaluable_expr(&field.value),
416
    }
417
509965562
}
418

            
419
/// Simplify an expression to a constant if possible
420
/// Returns:
421
/// `None` if the expression cannot be simplified to a constant (e.g. if it contains a variable)
422
/// `Some(Const)` if the expression can be simplified to a constant
423
2450011005
pub fn eval_constant(expr: &Expr) -> Option<Lit> {
424
2450011005
    match expr {
425
        Expr::TypeAnnotation(_, expr, _) | Expr::DomainAnnotation(_, expr, _) => {
426
29694
            eval_constant(expr)
427
        }
428
        Expr::Supset(_, a, b) => {
429
            let (
430
                Lit::AbstractLiteral(AbstractLiteral::Set(a)),
431
                Lit::AbstractLiteral(AbstractLiteral::Set(b)),
432
348
            ) = (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?)
433
            else {
434
                return None;
435
            };
436

            
437
348
            let a_set: HashSet<Lit> = a.iter().cloned().collect();
438
348
            let b_set: HashSet<Lit> = b.iter().cloned().collect();
439

            
440
348
            if a_set.difference(&b_set).count() > 0 {
441
261
                Some(Lit::Bool(a_set.is_superset(&b_set)))
442
            } else {
443
87
                Some(Lit::Bool(false))
444
            }
445
        }
446
        Expr::SupsetEq(_, a, b) => {
447
            let (
448
                Lit::AbstractLiteral(AbstractLiteral::Set(a)),
449
                Lit::AbstractLiteral(AbstractLiteral::Set(b)),
450
348
            ) = (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?)
451
            else {
452
                return None;
453
            };
454

            
455
348
            Some(Lit::Bool(
456
348
                a.iter()
457
348
                    .cloned()
458
348
                    .collect::<HashSet<Lit>>()
459
348
                    .is_superset(&b.iter().cloned().collect::<HashSet<Lit>>()),
460
348
            ))
461
        }
462
        Expr::Subset(_, a, b) => {
463
            let (
464
                Lit::AbstractLiteral(AbstractLiteral::Set(a)),
465
                Lit::AbstractLiteral(AbstractLiteral::Set(b)),
466
96889
            ) = (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?)
467
            else {
468
                return None;
469
            };
470

            
471
435
            let a_set: HashSet<Lit> = a.iter().cloned().collect();
472
435
            let b_set: HashSet<Lit> = b.iter().cloned().collect();
473

            
474
435
            if b_set.difference(&a_set).count() > 0 {
475
348
                Some(Lit::Bool(a_set.is_subset(&b_set)))
476
            } else {
477
87
                Some(Lit::Bool(false))
478
            }
479
        }
480
        Expr::SubsetEq(_, a, b) => {
481
            let (
482
                Lit::AbstractLiteral(AbstractLiteral::Set(a)),
483
                Lit::AbstractLiteral(AbstractLiteral::Set(b)),
484
179135
            ) = (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?)
485
            else {
486
                return None;
487
            };
488

            
489
377
            Some(Lit::Bool(
490
377
                a.iter()
491
377
                    .cloned()
492
377
                    .collect::<HashSet<Lit>>()
493
377
                    .is_subset(&b.iter().cloned().collect::<HashSet<Lit>>()),
494
377
            ))
495
        }
496
        Expr::Intersect(_, a, b) => {
497
            let (
498
                Lit::AbstractLiteral(AbstractLiteral::Set(a)),
499
                Lit::AbstractLiteral(AbstractLiteral::Set(b)),
500
33060
            ) = (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?)
501
            else {
502
                return None;
503
            };
504

            
505
15747
            let mut res: Vec<Lit> = Vec::new();
506
15747
            for lit in a {
507
28913
                if b.contains(&lit) && !res.contains(&lit) {
508
15573
                    res.push(lit);
509
13340
                }
510
            }
511
15747
            Some(Lit::AbstractLiteral(AbstractLiteral::Set(res)))
512
        }
513
        Expr::Difference(_, a, b) => {
514
            let (
515
                Lit::AbstractLiteral(AbstractLiteral::Set(a)),
516
                Lit::AbstractLiteral(AbstractLiteral::Set(b)),
517
5199
            ) = (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?)
518
            else {
519
                return None;
520
            };
521

            
522
4205
            let mut res: Vec<Lit> = Vec::new();
523
4205
            for lit in a {
524
12615
                if !b.contains(&lit) && !res.contains(&lit) {
525
8410
                    res.push(lit);
526
4205
                }
527
            }
528
4205
            Some(Lit::AbstractLiteral(AbstractLiteral::Set(res)))
529
        }
530
        Expr::Union(_, a, b) => {
531
            let (
532
                Lit::AbstractLiteral(AbstractLiteral::Set(a)),
533
                Lit::AbstractLiteral(AbstractLiteral::Set(b)),
534
16921
            ) = (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?)
535
            else {
536
                return None;
537
            };
538

            
539
13514
            let mut res: Vec<Lit> = Vec::new();
540
13514
            for lit in a {
541
26506
                res.push(lit);
542
            }
543
13514
            for lit in b {
544
26506
                if !res.contains(&lit) {
545
17371
                    res.push(lit);
546
9135
                }
547
            }
548
13514
            Some(Lit::AbstractLiteral(AbstractLiteral::Set(res)))
549
        }
550
        Expr::In(_, a, b) => {
551
5905408
            let member = eval_constant(a)?;
552
4621085
            let collection = eval_constant(b)?;
553
82844
            let values = generator_values_from_constant_collection(&collection)?;
554
141875
            Some(Lit::Bool(values.iter().any(|value| {
555
141875
                value.essence_cmp(&member) == CmpOrdering::Equal
556
141875
            })))
557
        }
558
        Expr::FromSolution(_, _) => None,
559
        Expr::DominanceRelation(_, _) => None,
560
        Expr::InDomain(_, e, domain) => {
561
28246
            let Expr::Atomic(_, Atom::Literal(lit)) = e.as_ref() else {
562
28246
                return None;
563
            };
564

            
565
            domain.contains(lit).ok().map(Into::into)
566
        }
567
527240728
        Expr::Atomic(_, Atom::Literal(c)) => Some(normalise_constant_set(c.clone())),
568
        Expr::Atomic(_, Atom::Reference(reference)) => {
569
1070052148
            reference.resolve_constant().map(normalise_constant_set)
570
        }
571
99437336
        Expr::AbstractLiteral(_, a) => Some(normalise_constant_set(Lit::AbstractLiteral(
572
99437336
            a.clone().into_literals()?,
573
        ))),
574
        Expr::Comprehension(_, comprehension) => {
575
8982450
            eval_constant_comprehension(comprehension.as_ref())
576
        }
577
677151
        Expr::RecordField(_, rec, fld_name) => match eval_constant(rec.as_ref())? {
578
            Lit::AbstractLiteral(AbstractLiteral::Record(ents)) => {
579
8497
                for Field { name, value } in ents {
580
12731
                    if name.eq(fld_name) {
581
8497
                        return Some(value);
582
4234
                    }
583
                }
584
                None
585
            }
586
33873
            Lit::AbstractLiteral(AbstractLiteral::Variant(field)) if field.name == *fld_name => {
587
33873
                Some(field.value.clone())
588
            }
589
            _ => None,
590
        },
591
        Expr::UnsafeIndex(_, subject, indices) | Expr::SafeIndex(_, subject, indices) => {
592
80256385
            let subject: Lit = eval_constant(subject.as_ref())?;
593
13648554
            let indices: Vec<Lit> = indices
594
13648554
                .iter()
595
13648554
                .map(eval_constant)
596
13648554
                .collect::<Option<Vec<Lit>>>()?;
597

            
598
13014046
            match subject {
599
                Lit::AbstractLiteral(subject @ AbstractLiteral::Matrix(_, _)) => {
600
9987722
                    matrix::flatten_enumerate(subject)
601
36123853
                        .find(|(i, _)| i == &indices)
602
                        .map(|(_, x)| x)
603
                }
604
                Lit::AbstractLiteral(subject @ AbstractLiteral::Tuple(_)) => {
605
3026324
                    let AbstractLiteral::Tuple(elems) = subject else {
606
                        return None;
607
                    };
608

            
609
                    bug_assert!(indices.len() == 1, "nested tuples not supported yet");
610

            
611
3026324
                    let Lit::Int(index) = indices[0].clone() else {
612
                        return None;
613
                    };
614

            
615
3026324
                    if elems.len() < index as usize || index < 1 {
616
                        return None;
617
3026324
                    }
618

            
619
                    // -1 for 0-indexing vs 1-indexing
620
3026324
                    let item = elems[index as usize - 1].clone();
621

            
622
3026324
                    Some(item)
623
                }
624
                Lit::AbstractLiteral(subject @ AbstractLiteral::Record(_)) => {
625
                    let AbstractLiteral::Record(elems) = subject else {
626
                        return None;
627
                    };
628

            
629
                    bug_assert!(indices.len() == 1, "nested record not supported yet");
630

            
631
                    let Lit::Int(index) = indices[0].clone() else {
632
                        return None;
633
                    };
634

            
635
                    if elems.len() < index as usize || index < 1 {
636
                        return None;
637
                    }
638

            
639
                    // -1 for 0-indexing vs 1-indexing
640
                    let item = elems[index as usize - 1].clone();
641
                    Some(item.value)
642
                }
643
                _ => None,
644
            }
645
        }
646
        Expr::UnsafeSlice(_, subject, indices) | Expr::SafeSlice(_, subject, indices) => {
647
1592388
            let subject: Lit = eval_constant(subject.as_ref())?;
648
2117
            let Lit::AbstractLiteral(subject @ AbstractLiteral::Matrix(_, _)) = subject else {
649
                return None;
650
            };
651

            
652
2117
            let hole_dim = indices
653
2117
                .iter()
654
2117
                .cloned()
655
4234
                .position(|x| x.is_none())
656
2117
                .expect("slice expression should have a hole dimension");
657

            
658
2117
            let missing_domain = matrix::index_domains(&subject)[hole_dim].clone();
659

            
660
2117
            let indices: Vec<Option<Lit>> = indices
661
2117
                .iter()
662
2117
                .cloned()
663
4234
                .map(|x| {
664
                    // the outer option represents success of this iterator, the inner the index
665
                    // slice.
666
4234
                    match x {
667
2117
                        Some(x) => eval_constant(&x).map(Some),
668
2117
                        None => Some(None),
669
                    }
670
4234
                })
671
                .collect::<Option<Vec<Option<Lit>>>>()?;
672

            
673
2117
            let indices_in_slice: Vec<Vec<Lit>> = missing_domain
674
2117
                .values()
675
2117
                .ok()?
676
6351
                .map(|i| {
677
6351
                    let mut indices = indices.clone();
678
6351
                    indices[hole_dim] = Some(i);
679
                    // These unwraps will only fail if we have multiple holes.
680
                    // As this is invalid, panicking is fine.
681
12702
                    indices.into_iter().map(|x| x.unwrap()).collect_vec()
682
6351
                })
683
                .collect_vec();
684

            
685
            // Note: indices_in_slice is not necessarily sorted, so this is the best way.
686
2117
            let elems = matrix::flatten_enumerate(subject)
687
19053
                .filter(|(i, _)| indices_in_slice.contains(i))
688
                .map(|(_, elem)| elem)
689
                .collect();
690

            
691
2117
            Some(Lit::AbstractLiteral(into_matrix![elems]))
692
        }
693
343302
        Expr::Abs(_, e) => un_op::<i32, i32>(|a| a.abs(), e).map(Lit::Int),
694
54210092
        Expr::Eq(_, a, b) => Some(Lit::Bool(equal_constant_literals(
695
54210092
            &eval_constant(a)?,
696
7109735
            &eval_constant(b)?,
697
29
        )?)),
698
13071486
        Expr::Neq(_, a, b) => Some(Lit::Bool(!equal_constant_literals(
699
13071486
            &eval_constant(a)?,
700
4040918
            &eval_constant(b)?,
701
29
        )?)),
702
9808010
        Expr::Lt(_, a, b) => bin_op::<i32, bool>(|a, b| a < b, a, b).map(Lit::Bool),
703
2225734
        Expr::Gt(_, a, b) => bin_op::<i32, bool>(|a, b| a > b, a, b).map(Lit::Bool),
704
22823442
        Expr::Leq(_, a, b) => bin_op::<i32, bool>(|a, b| a <= b, a, b).map(Lit::Bool),
705
3066030
        Expr::Geq(_, a, b) => bin_op::<i32, bool>(|a, b| a >= b, a, b).map(Lit::Bool),
706
109873492
        Expr::Not(_, expr) => un_op::<bool, bool>(|e| !e, expr).map(Lit::Bool),
707
        Expr::And(_, e) => {
708
44910816
            vec_lit_op::<bool, bool>(|e| e.iter().all(|&e| e), e.as_ref()).map(Lit::Bool)
709
        }
710
1236502
        Expr::Table(_, _, _) | Expr::FlatTable(..) | Expr::FlatShortTable(..) => None,
711
510400
        Expr::NegativeTable(_, _, _) | Expr::ShortTable(_, _, _) => None,
712
683675
        Expr::AtLeast(_, _, _, _) => None,
713
143956
        Expr::AtMost(_, _, _, _) => None,
714
5771
        Expr::Gcc(_, _, _, _) | Expr::GccWeak(_, _, _, _) => None,
715
1152
        Expr::Root(_, _) => None,
716
        Expr::Or(_, es) => {
717
            // possibly cheating; definitely should be in partial eval instead
718
123585484
            for e in es.unwrap_list_cow()?.iter() {
719
330837848
                if let Expr::Atomic(_, Atom::Literal(Lit::Bool(true))) = e {
720
86579
                    return Some(Lit::Bool(true));
721
330751269
                };
722
            }
723

            
724
123043090
            vec_lit_op::<bool, bool>(|e| e.iter().any(|&e| e), es.as_ref()).map(Lit::Bool)
725
        }
726
        // A `catchUndef` still standing at constant-folding time has no bubble to consult, so the
727
        // inner expression is total here and the default is unreachable.
728
452806
        Expr::CatchUndef(_, a, _) => eval_constant(a),
729
14026150
        Expr::Imply(_, a, b) => bin_op::<bool, bool>(|a, b| !a || b, a, b).map(Lit::Bool),
730
8442711
        Expr::Iff(_, a, b) => bin_op::<bool, bool>(|a, b| a == b, a, b).map(Lit::Bool),
731
30575532
        Expr::Sum(_, exprs) => vec_lit_op::<i32, i32>(|e| e.iter().sum(), exprs).map(Lit::Int),
732
        Expr::Product(_, exprs) => {
733
14026005
            vec_lit_op::<i32, i32>(|e| e.iter().product(), exprs).map(Lit::Int)
734
        }
735
        Expr::FlatIneq(_, a, b, c) => {
736
444787
            let a: i32 = a.try_into().ok()?;
737
403929
            let b: i32 = b.try_into().ok()?;
738
            let c: i32 = c.try_into().ok()?;
739

            
740
            Some(Lit::Bool(a <= b + c))
741
        }
742
        Expr::FlatSumGeq(_, exprs, a) => {
743
166911
            let sum = exprs.iter().try_fold(0, |acc, atom: &Atom| {
744
166911
                let n: i32 = atom.try_into().ok()?;
745
70870
                let acc = acc + n;
746
70870
                Some(acc)
747
166911
            })?;
748

            
749
29
            Some(Lit::Bool(sum >= a.try_into().ok()?))
750
        }
751
        Expr::FlatSumLeq(_, exprs, a) => {
752
131775
            let sum = exprs.iter().try_fold(0, |acc, atom: &Atom| {
753
131775
                let n: i32 = atom.try_into().ok()?;
754
52679
                let acc = acc + n;
755
52679
                Some(acc)
756
131775
            })?;
757

            
758
29
            Some(Lit::Bool(sum >= a.try_into().ok()?))
759
        }
760
        Expr::FlatMinEq(_, vars, result) => {
761
319
            let min = vars
762
319
                .iter()
763
348
                .try_fold(None, |acc: Option<i32>, atom: &Atom| {
764
348
                    let n: i32 = atom.try_into().ok()?;
765
29
                    Some(Some(acc.map_or(n, |m| m.min(n))))
766
348
                })??;
767
            let result: i32 = result.try_into().ok()?;
768
            Some(Lit::Bool(min == result))
769
        }
770
        Expr::Min(_, e) => {
771
707079
            opt_vec_lit_op::<i32, i32>(|e| e.iter().min().copied(), e.as_ref()).map(Lit::Int)
772
        }
773
        Expr::Max(_, e) => {
774
598056
            opt_vec_lit_op::<i32, i32>(|e| e.iter().max().copied(), e.as_ref()).map(Lit::Int)
775
        }
776
        Expr::UnsafeDiv(_, a, b) | Expr::SafeDiv(_, a, b) => {
777
13761490
            super::floor_div(unwrap_expr::<i32>(a)?, unwrap_expr::<i32>(b)?).map(Lit::Int)
778
        }
779
        Expr::UnsafeMod(_, a, b) | Expr::SafeMod(_, a, b) => {
780
7843875
            super::floor_mod(unwrap_expr::<i32>(a)?, unwrap_expr::<i32>(b)?).map(Lit::Int)
781
        }
782
11542
        Expr::Substring(_, s, t) => match (s.as_ref(), t.as_ref()) {
783
            (
784
                Expr::Atomic(_, Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Sequence(s)))),
785
                Expr::Atomic(_, Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Sequence(t)))),
786
            ) => {
787
                if s.len() > t.len() {
788
                    return Some(Lit::Bool(false));
789
                }
790

            
791
                let found = t.windows(s.len()).any(|window| window == s.as_slice());
792
                Some(Lit::Bool(found))
793
            }
794
11542
            _ => None,
795
        },
796
2523
        Expr::Subsequence(_, s, t) => match (s.as_ref(), t.as_ref()) {
797
            (
798
                Expr::Atomic(_, Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Sequence(s)))),
799
                Expr::Atomic(_, Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Sequence(t)))),
800
            ) => {
801
                let mut i = 0;
802
                let mut j = 0;
803

            
804
                while i < s.len() && j < t.len() {
805
                    if s[i] == t[j] {
806
                        i += 1;
807
                    }
808
                    j += 1;
809
                }
810

            
811
                Some(Lit::Bool(i == s.len()))
812
            }
813
2523
            _ => None,
814
        },
815
        Expr::MinionDivEqUndefZero(_, a, b, c) => {
816
            // div always rounds down
817
122046
            let a: i32 = a.try_into().ok()?;
818
29
            let b: i32 = b.try_into().ok()?;
819
            let c: i32 = c.try_into().ok()?;
820

            
821
            if b == 0 {
822
                return None;
823
            }
824

            
825
            let a = a as f32;
826
            let b = b as f32;
827
            let div: i32 = (a / b).floor() as i32;
828
            Some(Lit::Bool(div == c))
829
        }
830
241803
        Expr::Bubble(_, a, b) => bin_op::<bool, bool>(|a, b| a && b, a, b).map(Lit::Bool),
831
        Expr::MinionReify(_, a, b) => {
832
1665
            let result = eval_constant(a)?;
833

            
834
1077
            let result: bool = result.try_into().ok()?;
835
1077
            let b: bool = b.try_into().ok()?;
836

            
837
            Some(Lit::Bool(b == result))
838
        }
839
        Expr::MinionReifyImply(_, a, b) => {
840
432
            let result = eval_constant(a)?;
841

            
842
            let result: bool = result.try_into().ok()?;
843
            let b: bool = b.try_into().ok()?;
844

            
845
            if b {
846
                Some(Lit::Bool(result))
847
            } else {
848
                Some(Lit::Bool(true))
849
            }
850
        }
851
        Expr::MinionModuloEqUndefZero(_, a, b, c) => {
852
            // From Savile Row. Same semantics as division.
853
            //
854
            //   a - (b * floor(a/b))
855
            //
856
            // We don't use % as it has the same semantics as /. We don't use / as we want to round
857
            // down instead, not towards zero.
858

            
859
124134
            let a: i32 = a.try_into().ok()?;
860
29
            let b: i32 = b.try_into().ok()?;
861
            let c: i32 = c.try_into().ok()?;
862

            
863
            if b == 0 {
864
                return None;
865
            }
866

            
867
            let modulo = super::floor_mod(a, b)?;
868
            Some(Lit::Bool(modulo == c))
869
        }
870
        Expr::MinionPow(_, a, b, c) => {
871
            // only available for positive a b c
872

            
873
1421
            let a: i32 = a.try_into().ok()?;
874
            let b: i32 = b.try_into().ok()?;
875
            let c: i32 = c.try_into().ok()?;
876

            
877
            if a <= 0 {
878
                return None;
879
            }
880

            
881
            if b <= 0 {
882
                return None;
883
            }
884

            
885
            if c <= 0 {
886
                return None;
887
            }
888

            
889
            Some(Lit::Bool(a ^ b == c))
890
        }
891
261
        Expr::MinionWInSet(_, _, _) => None,
892
        Expr::MinionWInIntervalSet(_, x, intervals) => {
893
609
            let x_lit: &Lit = x.try_into().ok()?;
894

            
895
            let x_lit = match x_lit.clone() {
896
                Lit::Int(i) => Some(i),
897
                Lit::Bool(true) => Some(1),
898
                Lit::Bool(false) => Some(0),
899
                _ => None,
900
            }?;
901

            
902
            let mut intervals = intervals.iter();
903
            while let Some(lower) = intervals.next() {
904
                let Some(upper) = intervals.next() else {
905
                    break;
906
                };
907
                if &x_lit >= lower && &x_lit <= upper {
908
                    return Some(Lit::Bool(true));
909
                }
910
            }
911

            
912
            Some(Lit::Bool(false))
913
        }
914
        Expr::Flatten(_, _, _) => {
915
            // TODO
916
3044826
            None
917
        }
918
        // Constant folding treats the SMT-native form exactly like the one it came from.
919
        Expr::AllDiff(_, e) | Expr::SmtDistinct(_, e) => {
920
1500445
            let es = e.unwrap_list_cow()?;
921
29232
            let mut lits: HashSet<Lit> = HashSet::new();
922
29232
            for expr in es.iter() {
923
33814
                let Expr::Atomic(_, Atom::Literal(x)) = expr else {
924
27028
                    return None;
925
                };
926
6786
                match x {
927
                    Lit::Int(_) | Lit::Bool(_) | Lit::EnumVariant(_) | Lit::Unnamed(_) => {
928
6786
                        if lits.contains(x) {
929
                            return Some(Lit::Bool(false));
930
                        } else {
931
6786
                            lits.insert(x.clone());
932
                        }
933
                    }
934
                    Lit::AbstractLiteral(_) => return None, // Reject AbstractLiteral cases
935
                }
936
            }
937
2204
            Some(Lit::Bool(true))
938
        }
939
        Expr::FlatAllDiff(_, es) => {
940
7523
            let mut lits: HashSet<Lit> = HashSet::new();
941
7523
            for atom in es {
942
7523
                let Atom::Literal(x) = atom else {
943
7523
                    return None;
944
                };
945

            
946
                match x {
947
                    Lit::Int(_) | Lit::Bool(_) | Lit::EnumVariant(_) | Lit::Unnamed(_) => {
948
                        if lits.contains(x) {
949
                            return Some(Lit::Bool(false));
950
                        } else {
951
                            lits.insert(x.clone());
952
                        }
953
                    }
954
                    Lit::AbstractLiteral(_) => return None, // Reject AbstractLiteral cases
955
                }
956
            }
957
            Some(Lit::Bool(true))
958
        }
959
1262859
        Expr::FlatWatchedLiteral(_, _, _) => None,
960
6297150
        Expr::AuxDeclaration(_, _, _) => None,
961
367056
        Expr::Neg(_, a) => match eval_constant(a.as_ref())? {
962
100455
            Lit::Int(a) => Some(Lit::Int(-a)),
963
            _ => None,
964
        },
965
2523
        Expr::Factorial(_, a) => match eval_constant(a.as_ref())? {
966
2523
            Lit::Int(a) => factorial_i32(a).map(Lit::Int),
967
            _ => None,
968
        },
969
19480985
        Expr::Minus(_, a, b) => bin_op::<i32, i32>(|a, b| a - b, a, b).map(Lit::Int),
970
        Expr::FlatMinusEq(_, a, b) => {
971
1856
            let a: i32 = a.try_into().ok()?;
972
            let b: i32 = b.try_into().ok()?;
973
            Some(Lit::Bool(a == -b))
974
        }
975
        Expr::FlatProductEq(_, a, b, c) => {
976
7732
            let a: i32 = a.try_into().ok()?;
977
            let b: i32 = b.try_into().ok()?;
978
            let c: i32 = c.try_into().ok()?;
979
            Some(Lit::Bool(a * b == c))
980
        }
981
        Expr::FlatWeightedSumLeq(_, cs, vs, total) => {
982
13293
            let cs: Vec<i32> = cs
983
13293
                .iter()
984
38247
                .map(|x| TryInto::<i32>::try_into(x).ok())
985
                .collect::<Option<Vec<i32>>>()?;
986
13293
            let vs: Vec<i32> = vs
987
13293
                .iter()
988
21732
                .map(|x| TryInto::<i32>::try_into(x).ok())
989
13293
                .collect::<Option<Vec<i32>>>()?;
990
            let total: i32 = total.try_into().ok()?;
991

            
992
            let sum: i32 = izip!(cs, vs).fold(0, |acc, (c, v)| acc + (c * v));
993

            
994
            Some(Lit::Bool(sum <= total))
995
        }
996
        Expr::FlatWeightedSumGeq(_, cs, vs, total) => {
997
12684
            let cs: Vec<i32> = cs
998
12684
                .iter()
999
36855
                .map(|x| TryInto::<i32>::try_into(x).ok())
                .collect::<Option<Vec<i32>>>()?;
12684
            let vs: Vec<i32> = vs
12684
                .iter()
21152
                .map(|x| TryInto::<i32>::try_into(x).ok())
12684
                .collect::<Option<Vec<i32>>>()?;
            let total: i32 = total.try_into().ok()?;
            let sum: i32 = izip!(cs, vs).fold(0, |acc, (c, v)| acc + (c * v));
            Some(Lit::Bool(sum >= total))
        }
        Expr::FlatAbsEq(_, x, y) => {
435
            let x: i32 = x.try_into().ok()?;
29
            let y: i32 = y.try_into().ok()?;
            Some(Lit::Bool(x == y.abs()))
        }
        Expr::UnsafePow(_, a, b) | Expr::SafePow(_, a, b) => {
1576233
            let a: &Atom = a.try_into().ok()?;
1487870
            let a: i32 = a.try_into().ok()?;
1098186
            let b: &Atom = b.try_into().ok()?;
1098186
            let b: i32 = b.try_into().ok()?;
1097838
            if (a != 0 || b != 0) && b >= 0 {
1097838
                Some(Lit::Int(a.pow(b as u32)))
            } else {
                None
            }
        }
        Expr::Metavar(_, _) => None,
24770
        Expr::MinionElementOne(_, _, _, _) => None,
        Expr::ToInt(_, expression) => {
101610989
            let lit = eval_constant(expression.as_ref())?;
30988280
            match lit {
                Lit::Int(_) => Some(lit),
16133490
                Lit::Bool(true) => Some(Lit::Int(1)),
14854790
                Lit::Bool(false) => Some(Lit::Int(0)),
                _ => None,
            }
        }
219078
        Expr::SatObjective(..) => None,
        Expr::SATInt(_, _, _, _) => {
            // TODO: If this SATInt is composed of literals, we should evaluate it back to an
            // integer literal.
            //
            // This is important because `is_all_constant` currently returns true for SATInts
            // containing no references. If we don't evaluate them here, bubble rules will skip
            // them (thinking they'll be constant-folded later), but they'll actually reach
            // the solver adaptors as un-encoded unsafe operations, causing panics.
36158139
            None
        }
        Expr::PairwiseSum(_, a, b) => {
            match (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?) {
                (Lit::Int(a_int), Lit::Int(b_int)) => Some(Lit::Int(a_int + b_int)),
                _ => None,
            }
        }
        Expr::PairwiseProduct(_, a, b) => {
            match (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?) {
                (Lit::Int(a_int), Lit::Int(b_int)) => Some(Lit::Int(a_int * b_int)),
                _ => None,
            }
        }
        Expr::Defined(_, f) => {
19981
            let Lit::AbstractLiteral(AbstractLiteral::Function(pairs)) = eval_constant(f)? else {
                return None;
            };
            Some(Lit::AbstractLiteral(AbstractLiteral::Set(
                pairs.into_iter().map(|(key, _)| key).collect(),
            )))
        }
        Expr::Range(_, f) => {
44718
            let Lit::AbstractLiteral(AbstractLiteral::Function(pairs)) = eval_constant(f)? else {
                return None;
            };
            let mut values = Vec::new();
            for (_, value) in pairs {
                if !values.contains(&value) {
                    values.push(value);
                }
            }
            Some(Lit::AbstractLiteral(AbstractLiteral::Set(values)))
        }
        Expr::Image(_, f, arg) => {
930268
            let arg = eval_constant(arg)?;
168380
            match eval_constant(f)? {
                Lit::AbstractLiteral(AbstractLiteral::Function(pairs)) => pairs
                    .into_iter()
                    .find(|(key, _)| *key == arg)
                    .map(|(_, value)| value),
                // A sequence is a function from int(1..n), so applying it reads the entry at
                // that one-based position. Out of range is undefined, so unevaluable.
                Lit::AbstractLiteral(AbstractLiteral::Sequence(values)) => {
                    let Lit::Int(index) = arg else {
                        return None;
                    };
                    usize::try_from(index)
                        .ok()
                        .filter(|index| *index >= 1)
                        .and_then(|index| values.get(index - 1).cloned())
                }
                // Cycle notation: find which cycle (if any) mentions `arg` and return the next
                // element in it (wrapping around); an element mentioned in no cycle is an
                // implicit fixed point, mapping to itself.
                Lit::AbstractLiteral(AbstractLiteral::Permutation(cycles)) => {
10591
                    for cycle in &cycles {
19070
                        if let Some(pos) = cycle.iter().position(|x| *x == arg) {
6356
                            return Some(cycle[(pos + 1) % cycle.len()].clone());
4237
                        }
                    }
4235
                    Some(arg)
                }
                _ => None,
            }
        }
        Expr::PreImage(_, f, img) => {
            let Lit::AbstractLiteral(AbstractLiteral::Function(pairs)) = eval_constant(f)? else {
                return None;
            };
            let img = eval_constant(img)?;
            let mut keys = Vec::new();
            for (key, value) in pairs {
                if value == img && !keys.contains(&key) {
                    keys.push(key);
                }
            }
            Some(Lit::AbstractLiteral(AbstractLiteral::Set(keys)))
        }
        // Not yet needed by any in-scope function case; the partial evaluator already refuses
        // these gracefully (Err(RuleNotApplicable)) rather than panicking.
        Expr::ImageSet(_, _, _) => None,
1450
        Expr::Inverse(_, _, _) => None,
        Expr::PermInverse(_, _) => None,
        Expr::Compose(_, _, _) => None,
        Expr::Restrict(_, _, _) => None,
        Expr::ToSet(_, _) => None,
        Expr::ToMSet(_, _) => None,
        Expr::ToRelation(_, _) => None,
        Expr::Active(_, variant, alternative) => {
            let Lit::AbstractLiteral(AbstractLiteral::Variant(field)) =
193258
                eval_constant(variant.as_ref())?
            else {
                return None;
            };
67746
            Some(Lit::Bool(field.name == *alternative))
        }
        Expr::RelationProj(_, _, _) => todo!(),
        // Not yet needed by any in-scope partition case; the partial evaluator already refuses
        // these gracefully (Err(RuleNotApplicable)) rather than panicking. Constant partition
        // literal equality/membership already works via the generic Literal equality path above,
        // without going through these operators at all.
        Expr::Apart(_, _, _) => None,
        Expr::Together(_, _, _) => None,
        Expr::Participants(_, _) => None,
        Expr::Party(_, _, _) => None,
        Expr::Parts(_, _) => None,
        Expr::Card(_, collection) => {
341493
            let Lit::AbstractLiteral(collection) = eval_constant(collection)? else {
                return None;
            };
141621
            let length = match collection {
130097
                AbstractLiteral::Set(values) => super::literals::distinct_literal_count(&values),
                AbstractLiteral::MSet(values)
                | AbstractLiteral::Sequence(values)
5173
                | AbstractLiteral::Matrix(values, _) => values.len(),
                AbstractLiteral::Function(entries) => entries.len(),
2117
                AbstractLiteral::Relation(entries) => entries.len(),
                // A permutation's cardinality is its numMoved count -- how many elements are
                // mentioned in some cycle -- not the number of cycles, matching the same
                // "unmentioned = fixed point" convention used everywhere else for permutations
                // (contains()'s size check, PermutationAsFunction's own numMoved structural
                // constraint).
4234
                AbstractLiteral::Permutation(cycles) => cycles.iter().flatten().count(),
                _ => return None,
            };
141621
            i32::try_from(length).ok().map(Lit::Int)
        }
        Expr::LexLt(_, a, b) => {
742603
            let lt = vec_expr_pairs_op::<i32, _>(a, b, |pairs, (a_len, b_len)| {
348
                pairs
348
                    .iter()
696
                    .find_map(|(a, b)| match a.cmp(b) {
174
                        CmpOrdering::Less => Some(true),     // First difference is <
                        CmpOrdering::Greater => Some(false), // First difference is >
522
                        CmpOrdering::Equal => None,          // No difference
696
                    })
348
                    .unwrap_or(a_len < b_len) // [1,1] <lex [1,1,x]
742255
            })?;
348
            Some(lt.into())
        }
        Expr::LexLeq(_, a, b) => {
766673
            let lt = vec_expr_pairs_op::<i32, _>(a, b, |pairs, (a_len, b_len)| {
174
                pairs
174
                    .iter()
348
                    .find_map(|(a, b)| match a.cmp(b) {
174
                        CmpOrdering::Less => Some(true),
                        CmpOrdering::Greater => Some(false),
174
                        CmpOrdering::Equal => None,
348
                    })
174
                    .unwrap_or(a_len <= b_len) // [1,1] <=lex [1,1,x]
766499
            })?;
174
            Some(lt.into())
        }
60349
        Expr::LexGt(_, a, b) => eval_constant(&Expr::LexLt(Metadata::new(), b.clone(), a.clone())),
        Expr::LexGeq(_, a, b) => {
130384
            eval_constant(&Expr::LexLeq(Metadata::new(), b.clone(), a.clone()))
        }
        Expr::FlatLexLt(_, a, b) => {
8845
            let lt = atoms_pairs_op::<i32, _>(a, b, |pairs, (a_len, b_len)| {
                pairs
                    .iter()
                    .find_map(|(a, b)| match a.cmp(b) {
                        CmpOrdering::Less => Some(true),
                        CmpOrdering::Greater => Some(false),
                        CmpOrdering::Equal => None,
                    })
                    .unwrap_or(a_len < b_len)
8845
            })?;
            Some(lt.into())
        }
        Expr::FlatLexLeq(_, a, b) => {
3074
            let lt = atoms_pairs_op::<i32, _>(a, b, |pairs, (a_len, b_len)| {
                pairs
                    .iter()
                    .find_map(|(a, b)| match a.cmp(b) {
                        CmpOrdering::Less => Some(true),
                        CmpOrdering::Greater => Some(false),
                        CmpOrdering::Equal => None,
                    })
                    .unwrap_or(a_len <= b_len)
3074
            })?;
            Some(lt.into())
        }
        Expr::Pred(_, expr) => {
            let ev = unwrap_expr::<EnumVariant>(expr)?;
            ev.pred().map(Lit::EnumVariant)
        }
        Expr::Succ(_, expr) => {
16
            let ev = unwrap_expr::<EnumVariant>(expr)?;
16
            ev.succ().map(Lit::EnumVariant)
        }
        Expr::AllDifferentExcept(_, _, _) | Expr::ElementId(_, _, _) | Expr::IndexOf(_, _, _) => {
1295546
            None
        }
        // Needs the target's domain to expand, which this function has no access to; the
        // fallback rewrite rule (passes::attribute_as_constraint) handles expansion instead.
        Expr::AttributeAsConstraint(_, _, _, _)
        | Expr::SatElement(_, _, _, _)
1290152
        | Expr::SatAllDifferentComparisons(_, _) => None,
    }
2450011005
}
110216794
pub fn un_op<T, A>(f: fn(T) -> A, a: &Expr) -> Option<A>
110216794
where
110216794
    T: TryFrom<Lit>,
{
110216794
    let a = unwrap_expr::<T>(a)?;
339413
    Some(f(a))
110216794
}
80114865
pub fn bin_op<T, A>(f: fn(T, T) -> A, a: &Expr, b: &Expr) -> Option<A>
80114865
where
80114865
    T: TryFrom<Lit>,
{
80114865
    let a = unwrap_expr::<T>(a)?;
41051475
    let b = unwrap_expr::<T>(b)?;
27165896
    Some(f(a, b))
80114865
}
#[allow(dead_code)]
pub fn tern_op<T, A>(f: fn(T, T, T) -> A, a: &Expr, b: &Expr, c: &Expr) -> Option<A>
where
    T: TryFrom<Lit>,
{
    let a = unwrap_expr::<T>(a)?;
    let b = unwrap_expr::<T>(b)?;
    let c = unwrap_expr::<T>(c)?;
    Some(f(a, b, c))
}
pub fn vec_op<T, A>(f: fn(Vec<T>) -> A, a: &[Expr]) -> Option<A>
where
    T: TryFrom<Lit>,
{
    let a = a.iter().map(unwrap_expr).collect::<Option<Vec<T>>>()?;
    Some(f(a))
}
212555443
pub fn vec_lit_op<T, A>(f: fn(Vec<T>) -> A, a: &Expr) -> Option<A>
212555443
where
212555443
    T: TryFrom<Lit>,
{
212555443
    Some(f(eval_list_items(a)?))
212555443
}
type PairsCallback<T, A> = fn(Vec<(T, T)>, (usize, usize)) -> A;
/// Calls the given function on each consecutive pair of elements in the list expressions.
/// Also passes the length of the two lists.
1509276
fn vec_expr_pairs_op<T, A>(a: &Expr, b: &Expr, f: PairsCallback<T, A>) -> Option<A>
1509276
where
1509276
    T: TryFrom<Lit>,
{
1509276
    let a_exprs = a.clone().unwrap_matrix_unchecked()?.0;
576781
    let b_exprs = b.clone().unwrap_matrix_unchecked()?.0;
576781
    let lens = (a_exprs.len(), b_exprs.len());
576781
    let lit_pairs = std::iter::zip(a_exprs, b_exprs)
577303
        .map(|(a, b)| Some((unwrap_expr(&a)?, unwrap_expr(&b)?)))
576259
        .collect::<Option<Vec<(T, T)>>>()?;
522
    Some(f(lit_pairs, lens))
1509276
}
/// Same as [`vec_expr_pairs_op`], but over slices of atoms.
11919
fn atoms_pairs_op<T, A>(a: &[Atom], b: &[Atom], f: PairsCallback<T, A>) -> Option<A>
11919
where
11919
    T: TryFrom<Atom>,
{
11919
    let lit_pairs = Iterator::zip(a.iter(), b.iter())
11919
        .map(|(a, b)| Some((a.clone().try_into().ok()?, b.clone().try_into().ok()?)))
11919
        .collect::<Option<Vec<(T, T)>>>()?;
    Some(f(lit_pairs, (a.len(), b.len())))
11919
}
pub fn opt_vec_op<T, A>(f: fn(Vec<T>) -> Option<A>, a: &[Expr]) -> Option<A>
where
    T: TryFrom<Lit>,
{
    let a = a.iter().map(unwrap_expr).collect::<Option<Vec<T>>>()?;
    f(a)
}
1305135
pub fn opt_vec_lit_op<T, A>(f: fn(Vec<T>) -> Option<A>, a: &Expr) -> Option<A>
1305135
where
1305135
    T: TryFrom<Lit>,
{
1305135
    f(eval_list_items(a)?)
1305135
}
#[allow(dead_code)]
pub fn flat_op<T, A>(f: fn(Vec<T>, T) -> A, a: &[Expr], b: &Expr) -> Option<A>
where
    T: TryFrom<Lit>,
{
    let a = a.iter().map(unwrap_expr).collect::<Option<Vec<T>>>()?;
    let b = unwrap_expr::<T>(b)?;
    Some(f(a, b))
}
497660767
pub fn unwrap_expr<T: TryFrom<Lit>>(expr: &Expr) -> Option<T> {
497660767
    let c = eval_constant(expr)?;
133656284
    TryInto::<T>::try_into(c).ok()
497660767
}
213860578
fn eval_list_items<T>(expr: &Expr) -> Option<Vec<T>>
213860578
where
213860578
    T: TryFrom<Lit>,
{
213860578
    if let Some((items, _)) = expr.unwrap_matrix_unchecked_ref() {
203175985
        return items.iter().map(unwrap_expr).collect();
10684593
    }
10684593
    let collection = eval_constant(expr)?;
6375036
    generator_values_from_constant_collection(&collection)?
        .into_iter()
6375036
        .map(TryInto::try_into)
        .collect::<Result<Vec<_>, _>>()
        .ok()
213860578
}
8982450
fn eval_constant_comprehension(comprehension: &Comprehension) -> Option<Lit> {
8982450
    let mut values = Vec::new();
8982450
    eval_comprehension_qualifiers(comprehension, 0, &mut values)?;
44844
    Some(Lit::AbstractLiteral(
44844
        AbstractLiteral::matrix_implied_indices(values),
44844
    ))
8982450
}
20485156
fn eval_comprehension_qualifiers(
20485156
    comprehension: &Comprehension,
20485156
    qualifier_index: usize,
20485156
    values: &mut Vec<Lit>,
20485156
) -> Option<()> {
20485156
    if qualifier_index == comprehension.qualifiers.len() {
7682208
        values.push(eval_constant(&comprehension.return_expression)?);
158439
        return Some(());
12802948
    }
12802948
    match &comprehension.qualifiers[qualifier_index] {
        ComprehensionQualifier::Generator { ptr } => {
10853395
            let domain = ptr.domain()?;
10853395
            let generator_values = domain
10853395
                .resolve()
10852959
                .and_then(|x| x.values())
436
                .ok()?
                .collect_vec();
10852959
            for value in generator_values {
10999882
                with_temporary_quantified_binding(ptr, &value, || {
10999882
                    eval_comprehension_qualifiers(comprehension, qualifier_index + 1, values)
10999882
                })?;
            }
        }
        ComprehensionQualifier::ExpressionGenerator { ptr } => {
            // clone immediately so the read lock guard is dropped
895496
            let expr = ptr.as_quantified_expr()?.clone();
895496
            let generator_values = generator_values_from_expr(&expr)?;
209964
            for value in generator_values {
272662
                with_temporary_quantified_binding(ptr, &value, || {
272662
                    eval_comprehension_qualifiers(comprehension, qualifier_index + 1, values)
272662
                })?;
            }
        }
1054057
        ComprehensionQualifier::Condition(condition) => match eval_constant(condition)? {
            Lit::Bool(true) => {
230162
                eval_comprehension_qualifiers(comprehension, qualifier_index + 1, values)?
            }
96026
            Lit::Bool(false) => {}
            _ => return None,
        },
    }
399426
    Some(())
20485156
}
/// Values for a constant collection expression used during constant folding.
///
/// This does not enumerate decision-variable domains; quantification over decisions is not
/// unrolled here.
895496
pub fn generator_values_from_expr(expr: &Expr) -> Option<Vec<Lit>> {
895496
    generator_values_from_constant_collection(&eval_constant(expr)?)
895496
}
6667844
pub(crate) fn generator_values_from_constant_collection(lit: &Lit) -> Option<Vec<Lit>> {
6667844
    match lit {
        Lit::AbstractLiteral(AbstractLiteral::Set(values))
        | Lit::AbstractLiteral(AbstractLiteral::MSet(values))
911844
        | Lit::AbstractLiteral(AbstractLiteral::Tuple(values)) => Some(values.clone()),
5756000
        Lit::AbstractLiteral(AbstractLiteral::Matrix(values, _)) => Some(values.clone()),
        Lit::AbstractLiteral(list) => list.unwrap_list().cloned(),
        _ => None,
    }
6667844
}
11272544
fn with_temporary_quantified_binding<T>(
11272544
    quantified: &crate::ast::DeclarationPtr,
11272544
    value: &Lit,
11272544
    f: impl FnOnce() -> Option<T>,
11272544
) -> Option<T> {
11272544
    let mut targets = vec![quantified.clone()];
11272544
    if let DeclarationKind::Quantified(inner) = &*quantified.kind()
10999882
        && let Some(generator) = inner.generator()
    {
        targets.push(generator.clone());
11272544
    }
11272544
    let mut originals = Vec::with_capacity(targets.len());
11272544
    for mut target in targets {
11272544
        let old_kind = target.replace_kind(DeclarationKind::TemporaryValueLetting(Expr::Atomic(
11272544
            Metadata::new(),
11272544
            Atom::Literal(value.clone()),
11272544
        )));
11272544
        originals.push((target, old_kind));
    }
11272544
    let result = f();
11272544
    for (mut target, old_kind) in originals.into_iter().rev() {
11272544
        let _ = target.replace_kind(old_kind);
    }
11272544
    result
11272544
}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::matrix_expr;
38
    fn int_lit(value: i32) -> Expr {
38
        Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Int(value)))
38
    }
7
    fn bool_lit(value: bool) -> Expr {
7
        Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Bool(value)))
7
    }
2
    fn variant_lit(alternative: &str, value: Expr) -> Expr {
2
        Expr::AbstractLiteral(
2
            Metadata::new(),
2
            AbstractLiteral::Variant(Moo::new(Field {
2
                name: crate::ast::Name::user(alternative),
2
                value,
2
            })),
2
        )
2
    }
16
    fn root(exprs: Vec<Expr>) -> Expr {
16
        Expr::Root(Metadata::new(), exprs)
16
    }
    #[test]
1
    fn evaluates_active_on_variant_literals() {
1
        let variant = Moo::new(variant_lit("value", int_lit(2)));
1
        let active = Expr::Active(
1
            Metadata::new(),
1
            variant.clone(),
1
            crate::ast::Name::user("value"),
1
        );
1
        let inactive = Expr::Active(Metadata::new(), variant, crate::ast::Name::user("flag"));
1
        assert_eq!(eval_constant(&active), Some(Lit::Bool(true)));
1
        assert_eq!(eval_constant(&inactive), Some(Lit::Bool(false)));
1
        assert!(run_partial_evaluator_local(&active).is_ok());
1
        let field = Expr::RecordField(
1
            Metadata::new(),
1
            Moo::new(variant_lit("value", int_lit(2))),
1
            crate::ast::Name::user("value"),
1
        );
1
        assert_eq!(eval_constant(&field), Some(Lit::Int(2)));
1
    }
    #[test]
1
    fn local_root_partial_eval_strips_true_constraints() {
1
        let expr = root(vec![bool_lit(true), int_lit(1)]);
1
        let Expr::Root(_, exprs) = &expr else {
            panic!("expected root");
        };
1
        let normalised = normalise_root_constraints_local(exprs).unwrap();
1
        assert_eq!(normalised, root(vec![int_lit(1)]));
1
    }
    #[test]
1
    fn local_root_partial_eval_propagates_false() {
1
        let expr = root(vec![bool_lit(false), int_lit(1)]);
1
        let Expr::Root(_, exprs) = &expr else {
            panic!("expected root");
        };
1
        let normalised = normalise_root_constraints_local(exprs).unwrap();
1
        assert_eq!(normalised, root(vec![bool_lit(false)]));
1
    }
    #[test]
1
    fn deep_root_normalisation_folds_ground_constraint() {
1
        let expr = root(vec![Expr::Sum(
1
            Metadata::new(),
1
            Moo::new(matrix_expr![int_lit(1), int_lit(2), int_lit(3)]),
1
        )]);
1
        let normalised = normalise_root_constraints_deep(&expr).unwrap();
1
        assert_eq!(normalised, root(vec![int_lit(6)]));
1
    }
    #[test]
1
    fn deep_root_normalisation_applies_partial_eval_steps() {
1
        let expr = root(vec![Expr::Or(
1
            Metadata::new(),
1
            Moo::new(matrix_expr![bool_lit(false), int_lit(1)]),
1
        )]);
1
        let normalised = normalise_root_constraints_deep(&expr).unwrap();
1
        assert_eq!(
1
            normalised,
1
            root(vec![Expr::Or(
1
                Metadata::new(),
1
                Moo::new(matrix_expr![int_lit(1)]),
1
            )])
        );
1
    }
    #[test]
1
    fn selective_deep_root_normalisation_skips_solver_flat_constraints() {
1
        let flat = Expr::FlatProductEq(
1
            Metadata::new(),
1
            Moo::new(Atom::Literal(Lit::Int(1))),
1
            Moo::new(Atom::Literal(Lit::Int(2))),
1
            Moo::new(Atom::Literal(Lit::Int(3))),
1
        );
1
        let expr = root(vec![bool_lit(true), flat]);
1
        assert!(normalise_root_constraints_deep(&expr).is_none());
1
    }
    #[test]
1
    fn deep_root_normalisation_terminates_on_already_folded_constraint() {
1
        let expr = root(vec![int_lit(5)]);
1
        assert!(normalise_root_constraints_deep(&expr).is_none());
1
    }
    #[test]
1
    fn local_evaluator_normalisation_terminates_on_already_folded_literal() {
1
        let expr = int_lit(5);
1
        assert!(normalise_evaluator_local(&expr).is_none());
1
    }
    #[test]
1
    fn constant_set_cardinality_is_folded() {
1
        let set = Expr::Atomic(
1
            Metadata::new(),
1
            Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Set(vec![
1
                Lit::Int(1),
1
                Lit::Int(2),
1
            ]))),
1
        );
1
        let cardinality = Expr::Card(Metadata::new(), Moo::new(set));
1
        assert_eq!(eval_constant(&cardinality), Some(Lit::Int(2)));
1
    }
    #[test]
1
    fn constant_set_duplicates_are_normalised_before_cardinality() {
1
        let literal = Lit::AbstractLiteral(AbstractLiteral::Set(vec![1.into(), 1.into()]));
1
        let atomic = Expr::from(literal);
1
        let expression = Expr::AbstractLiteral(
1
            Metadata::new(),
1
            AbstractLiteral::Set(vec![1.into(), 1.into()]),
1
        );
1
        for set in [atomic, expression] {
2
            let cardinality = Expr::Card(Metadata::new(), Moo::new(set));
2
            assert_eq!(eval_constant(&cardinality), Some(Lit::Int(1)));
        }
1
        let multiset = Expr::from(Lit::AbstractLiteral(AbstractLiteral::MSet(vec![
1
            1.into(),
1
            1.into(),
1
        ])));
1
        assert_eq!(
1
            eval_constant(&Expr::Card(Metadata::new(), Moo::new(multiset))),
1
            Some(Lit::Int(2))
        );
1
    }
    #[test]
1
    fn constant_set_minimum_and_maximum_are_folded() {
1
        let set = Expr::from(Lit::AbstractLiteral(AbstractLiteral::Set(vec![
1
            Lit::Int(4),
1
            Lit::Int(1),
1
            Lit::Int(3),
1
        ])));
1
        assert_eq!(
1
            eval_constant(&Expr::Min(Metadata::new(), Moo::new(set.clone()))),
1
            Some(Lit::Int(1))
        );
1
        assert_eq!(
1
            eval_constant(&Expr::Max(Metadata::new(), Moo::new(set))),
1
            Some(Lit::Int(4))
        );
1
    }
    #[test]
1
    fn constant_set_membership_supports_composite_values() {
        let member =
1
            Lit::AbstractLiteral(AbstractLiteral::Tuple(vec![Lit::Bool(true), Lit::Int(2)]));
1
        let set = Lit::AbstractLiteral(AbstractLiteral::Set(vec![member.clone()]));
1
        let membership = Expr::In(
1
            Metadata::new(),
1
            Moo::new(Expr::from(member)),
1
            Moo::new(Expr::from(set)),
1
        );
1
        assert_eq!(eval_constant(&membership), Some(Lit::Bool(true)));
1
    }
    #[test]
1
    fn constant_equality_supports_composite_values() {
1
        let tuple = Lit::AbstractLiteral(AbstractLiteral::Tuple(vec![Lit::Int(1), Lit::Int(3)]));
1
        let equal = Expr::Eq(
1
            Metadata::new(),
1
            Moo::new(Expr::from(tuple.clone())),
1
            Moo::new(Expr::from(tuple)),
1
        );
1
        let unequal = Expr::Neq(
1
            Metadata::new(),
1
            Moo::new(Expr::from(Lit::AbstractLiteral(AbstractLiteral::Tuple(
1
                vec![Lit::Int(1), Lit::Int(3)],
1
            )))),
1
            Moo::new(Expr::from(Lit::AbstractLiteral(AbstractLiteral::Tuple(
1
                vec![Lit::Int(1), Lit::Int(4)],
1
            )))),
1
        );
1
        assert_eq!(eval_constant(&equal), Some(Lit::Bool(true)));
1
        assert_eq!(eval_constant(&unequal), Some(Lit::Bool(true)));
1
        let set_equality = Expr::Eq(
1
            Metadata::new(),
1
            Moo::new(Expr::from(Lit::AbstractLiteral(AbstractLiteral::Set(
1
                vec![Lit::Int(1), Lit::Int(2)],
1
            )))),
1
            Moo::new(Expr::from(Lit::AbstractLiteral(AbstractLiteral::Set(
1
                vec![Lit::Int(2), Lit::Int(1)],
1
            )))),
1
        );
1
        assert_eq!(eval_constant(&set_equality), Some(Lit::Bool(true)));
1
    }
    #[test]
1
    fn finish_root_normalisation_applies_local_root_rules() {
1
        let expr = root(vec![bool_lit(true), int_lit(1)]);
1
        let normalised = finish_root_evaluator_normalisation(&expr).unwrap();
1
        assert_eq!(normalised, root(vec![int_lit(1)]));
1
    }
    #[test]
1
    fn selective_deep_only_constraint_folds_target_index() {
1
        let ground = Expr::Sum(
1
            Metadata::new(),
1
            Moo::new(matrix_expr![int_lit(1), int_lit(2), int_lit(3)]),
1
        );
1
        let untouched = Expr::Sum(
1
            Metadata::new(),
1
            Moo::new(matrix_expr![int_lit(4), int_lit(5)]),
1
        );
1
        let expr = root(vec![ground, untouched.clone()]);
1
        let normalised = normalise_root_selective_deep_expr(&expr, Some(0)).unwrap();
1
        assert_eq!(normalised, root(vec![int_lit(6), untouched]));
1
    }
    #[test]
1
    fn finish_root_normalisation_flattens_top_level_and() {
1
        let expr = root(vec![Expr::And(
1
            Metadata::new(),
1
            Moo::new(matrix_expr![bool_lit(true), int_lit(1), int_lit(2)]),
1
        )]);
1
        let normalised = finish_root_evaluator_normalisation(&expr).unwrap();
1
        assert_eq!(normalised, root(vec![int_lit(1), int_lit(2)]));
1
    }
2
    fn permutation_lit(cycles: Vec<Vec<Expr>>) -> Expr {
2
        Expr::AbstractLiteral(Metadata::new(), AbstractLiteral::Permutation(cycles))
2
    }
    #[test]
1
    fn evaluates_image_on_a_permutation_literal_moved_point() {
1
        let p = Moo::new(permutation_lit(vec![
1
            vec![int_lit(1), int_lit(3), int_lit(5)],
1
            vec![int_lit(2), int_lit(4)],
1
        ]));
1
        let image_1 = Expr::Image(Metadata::new(), p.clone(), Moo::new(int_lit(1)));
1
        let image_3 = Expr::Image(Metadata::new(), p.clone(), Moo::new(int_lit(3)));
1
        let image_5 = Expr::Image(Metadata::new(), p.clone(), Moo::new(int_lit(5)));
1
        let image_2 = Expr::Image(Metadata::new(), p.clone(), Moo::new(int_lit(2)));
1
        let image_4 = Expr::Image(Metadata::new(), p, Moo::new(int_lit(4)));
1
        assert_eq!(eval_constant(&image_1), Some(Lit::Int(3)));
1
        assert_eq!(eval_constant(&image_3), Some(Lit::Int(5)));
1
        assert_eq!(eval_constant(&image_5), Some(Lit::Int(1)));
1
        assert_eq!(eval_constant(&image_2), Some(Lit::Int(4)));
1
        assert_eq!(eval_constant(&image_4), Some(Lit::Int(2)));
1
    }
    #[test]
1
    fn evaluates_image_on_a_permutation_literal_fixed_point() {
1
        let p = Moo::new(permutation_lit(vec![vec![int_lit(1), int_lit(2)]]));
1
        let image_3 = Expr::Image(Metadata::new(), p, Moo::new(int_lit(3)));
        // 3 is not mentioned in any cycle, so it is an implicit fixed point.
1
        assert_eq!(eval_constant(&image_3), Some(Lit::Int(3)));
1
    }
}