Skip to main content

conjure_cp_core/ast/
eval.rs

1#![allow(dead_code)]
2use crate::ast::{
3    AbstractLiteral, Atom, DeclarationKind, Expression as Expr, Field, Literal as Lit, Metadata,
4    Moo,
5    comprehension::{Comprehension, ComprehensionQualifier},
6    matrix,
7};
8use crate::bug_assert;
9use crate::into_matrix;
10use itertools::{Itertools as _, izip};
11use std::cmp::Ordering as CmpOrdering;
12use std::collections::HashSet;
13
14use super::partial_eval::{run_partial_evaluator, run_partial_evaluator_local};
15
16pub(crate) fn factorial_i32(n: i32) -> Option<i32> {
17    if n < 0 {
18        return None;
19    }
20
21    (1..=n).try_fold(1_i32, i32::checked_mul)
22}
23
24fn eval_constant_set(expr: &Expr) -> Option<Vec<Lit>> {
25    let Lit::AbstractLiteral(AbstractLiteral::Set(values)) = eval_constant(expr)? else {
26        return None;
27    };
28
29    Some(values)
30}
31
32/// Compare literals only when their outer Essence literal kinds agree.
33///
34/// Inner types are guaranteed by expression type checking; keeping this guard here also makes
35/// direct evaluator calls on unlike primitive or abstract literal kinds safely unevaluable.
36fn equal_constant_literals(lhs: &Lit, rhs: &Lit) -> Option<bool> {
37    match (lhs, rhs) {
38        (Lit::Int(_), Lit::Int(_)) | (Lit::Bool(_), Lit::Bool(_)) => {
39            Some(lhs.essence_cmp(rhs) == CmpOrdering::Equal)
40        }
41        (Lit::AbstractLiteral(lhs_abstract), Lit::AbstractLiteral(rhs_abstract))
42            if std::mem::discriminant(lhs_abstract) == std::mem::discriminant(rhs_abstract) =>
43        {
44            Some(lhs.essence_cmp(rhs) == CmpOrdering::Equal)
45        }
46        _ => None,
47    }
48}
49
50/// Simplify an expression to a constant using only constants already present at this node.
51///
52/// This is intended for the rewriter: child expressions should have been simplified by the
53/// scheduler before their parent is considered. Use [`eval_constant`] when a caller explicitly
54/// wants recursive evaluation of an arbitrary expression.
55pub fn eval_constant_local(expr: &Expr) -> Option<Lit> {
56    if !has_only_locally_evaluable_operands(expr) {
57        return None;
58    }
59
60    eval_constant(expr)
61}
62
63/// Applies the evaluator normalisation hook to a focused expression.
64///
65/// Evaluators are privileged simplifications, not ordinary rewrite rules. The rewriter invokes
66/// this hook before normal rule scheduling and immediately after a successful ordinary rule,
67/// walking upward while evaluation keeps simplifying parents. This exploits the semantic property
68/// that local constant and partial evaluation is always preferable to trying lower-priority rules,
69/// while avoiding millions of failed universal `constant_evaluator` rule attempts.
70///
71/// Away from [`Expr::Root`], the hook is pure and local: it does not create auxiliaries, mutate
72/// the symbol table, or recursively inspect arbitrary descendants. Children are expected to have
73/// been normalised by the scheduler before their parent is evaluated.
74///
75/// At [`Expr::Root`], a selective deep pass runs over top-level constraints (skipping solver-flat
76/// forms). Callers that know which root child changed should prefer
77/// [`normalise_root_selective_deep_expr`] with `only_constraint` so sibling constraints are not
78/// re-traversed. Local root-list reshaping (flatten top-level `and`) is intentionally deferred to
79/// [`finish_root_evaluator_normalisation`]: doing it mid-loop materialises huge root lists and
80/// explodes worklist rule attempts.
81pub fn normalise_evaluator_local(expr: &Expr) -> Option<Expr> {
82    match expr {
83        Expr::Root(_, exprs) => normalise_root_constraints_selective_deep(exprs, None),
84        // Focused `AbstractLiteral` literals must not repeatedly refold to themselves; parents
85        // still see the `Atomic(Literal(...))` form when the hook walks upward.
86        Expr::Atomic(_, Atom::Literal(Lit::AbstractLiteral(_))) => None,
87        _ => fold_constant_expression_local(expr).or_else(|| {
88            run_partial_evaluator_local(expr)
89                .ok()
90                .map(|reduction| reduction.new_expression)
91        }),
92    }
93    .filter(|new_expr| new_expr != expr)
94}
95
96/// Applies local root-list partial evaluation (strip `true`, propagate `false`, flatten `and`s).
97pub fn normalise_root_constraints_local(exprs: &[Expr]) -> Option<Expr> {
98    if exprs.is_empty() {
99        return Some(Expr::Root(Metadata::new(), vec![true.into()]));
100    }
101
102    let root = Expr::Root(Metadata::new(), exprs.to_vec());
103    run_partial_evaluator_local(&root)
104        .ok()
105        .map(|reduction| reduction.new_expression)
106        .filter(|new_root| new_root != &root)
107}
108
109/// Deep-normalises selected non-flat top-level constraints in `root`.
110pub fn normalise_root_selective_deep_expr(
111    root: &Expr,
112    only_constraint: Option<usize>,
113) -> Option<Expr> {
114    let Expr::Root(_, exprs) = root else {
115        return None;
116    };
117
118    normalise_root_constraints_selective_deep(exprs, only_constraint)
119}
120
121pub fn normalise_root_constraints_deep(root: &Expr) -> Option<Expr> {
122    normalise_root_selective_deep_expr(root, None)
123}
124
125/// Finishes evaluator normalisation on the model root after rewriting completes.
126///
127/// Applies local root-list partial evaluation to a fixpoint (strip `true`, propagate `false`,
128/// flatten top-level `and`). Deep evaluation of individual constraints already runs during
129/// rewriting via [`normalise_evaluator_local`] / [`normalise_root_selective_deep_expr`]; this
130/// finish pass must not be inlined into the mid-loop Root hook because flattening `and` early
131/// explodes worklist size on large expansions.
132pub fn finish_root_evaluator_normalisation(root: &Expr) -> Option<Expr> {
133    let Expr::Root(_, exprs) = root else {
134        return None;
135    };
136
137    let mut current = Expr::Root(Metadata::new(), exprs.clone());
138    let mut changed = false;
139    while let Expr::Root(_, current_exprs) = &current {
140        let Some(next) = normalise_root_constraints_local(current_exprs) else {
141            break;
142        };
143        current = next;
144        changed = true;
145    }
146    changed.then_some(current)
147}
148
149/// Deep-normalises a single top-level constraint, or returns `None` if it is already normal.
150///
151/// Callers that know which root child changed should prefer this over rebuilding the whole root:
152/// the root hook runs after every rewrite, so touching siblings there costs O(model size) per
153/// rewrite.
154pub fn normalise_root_constraint_deep(constraint: &Expr) -> Option<Expr> {
155    if constraint_skips_deep_root_normalisation(constraint) {
156        return None;
157    }
158
159    normalise_constraint_deep_to_fixpoint(constraint)
160}
161
162fn normalise_root_constraints_selective_deep(
163    exprs: &[Expr],
164    only_constraint: Option<usize>,
165) -> Option<Expr> {
166    if exprs.is_empty() {
167        return Some(Expr::Root(Metadata::new(), vec![true.into()]));
168    }
169
170    // Normalise first and only rebuild the root list once something has actually changed. Building
171    // the replacement eagerly clones every sibling constraint on each call, which dominates the
172    // post-rewrite root hook on models with many top-level constraints.
173    let mut normalised: Vec<(usize, Expr)> = Vec::new();
174    for (index, constraint) in exprs.iter().enumerate() {
175        if only_constraint.is_some_and(|only| only != index) {
176            continue;
177        }
178
179        if let Some(replacement) = normalise_root_constraint_deep(constraint) {
180            normalised.push((index, replacement));
181        }
182    }
183
184    if normalised.is_empty() {
185        return None;
186    }
187
188    let mut constraints = exprs.to_vec();
189    for (index, replacement) in normalised {
190        constraints[index] = replacement;
191    }
192
193    Some(Expr::Root(Metadata::new(), constraints))
194}
195
196/// Whether a top-level constraint has already been lowered to solver-flat form.
197///
198/// Deep root normalisation must not rewrite these: doing so can disturb auxiliaries introduced
199/// for Minion (for example chained `FlatProductEq` constraints) or repeat expensive work.
200fn constraint_skips_deep_root_normalisation(expr: &Expr) -> bool {
201    match expr {
202        Expr::FlatProductEq(_, _, _, _)
203        | Expr::FlatSumLeq(_, _, _)
204        | Expr::FlatSumGeq(_, _, _)
205        | Expr::FlatMinEq(_, _, _)
206        | Expr::FlatIneq(_, _, _, _)
207        | Expr::FlatMinusEq(_, _, _)
208        | Expr::FlatAbsEq(_, _, _)
209        | Expr::FlatAllDiff(_, _)
210        | Expr::SmtDistinct(_, _)
211        | Expr::FlatWeightedSumLeq(_, _, _, _)
212        | Expr::FlatWeightedSumGeq(_, _, _, _)
213        | Expr::FlatWatchedLiteral(_, _, _)
214        | Expr::MinionDivEqUndefZero(_, _, _, _)
215        | Expr::MinionModuloEqUndefZero(_, _, _, _)
216        | Expr::MinionPow(_, _, _, _)
217        | Expr::MinionReify(_, _, _)
218        | Expr::MinionReifyImply(_, _, _)
219        | Expr::MinionWInIntervalSet(_, _, _)
220        | Expr::MinionWInSet(_, _, _)
221        | Expr::MinionElementOne(_, _, _, _) => true,
222        Expr::AuxDeclaration(_, _, inner) => matches!(
223            inner.as_ref(),
224            Expr::Product(_, _) | Expr::FlatProductEq(_, _, _, _)
225        ),
226        _ => false,
227    }
228}
229
230fn fold_constant_expression_deep(expr: &Expr) -> Option<Expr> {
231    let constant = eval_constant(expr)?;
232    fold_constant_expression(expr, constant).filter(|folded| folded != expr)
233}
234
235fn normalise_constraint_deep_to_fixpoint(constraint: &Expr) -> Option<Expr> {
236    if matches!(constraint, Expr::Atomic(_, Atom::Literal(_))) {
237        return None;
238    }
239
240    let mut current = constraint.clone();
241    let mut changed = false;
242
243    while let Some(step) = fold_constant_expression_deep(&current)
244        .or_else(|| partial_evaluator_deep_step(&current))
245        .filter(|step| step != &current)
246    {
247        current = step;
248        changed = true;
249    }
250
251    changed
252        .then_some(current)
253        .filter(|current| current != constraint)
254}
255
256fn partial_evaluator_deep_step(expr: &Expr) -> Option<Expr> {
257    run_partial_evaluator(expr)
258        .ok()
259        .map(|reduction| reduction.new_expression)
260        .filter(|new_expr| new_expr != expr)
261}
262
263/// Constant-folds `expr` locally unless doing so would inline a referenced matrix literal.
264fn fold_constant_expression_local(expr: &Expr) -> Option<Expr> {
265    let constant = match expr {
266        // Comprehensions are atomic in arena traversal; evaluate them in one step here rather than
267        // via operand checks that would repeat the same work for every parent.
268        Expr::Comprehension(_, _) => eval_constant(expr)?,
269        _ => eval_constant_local(expr)?,
270    };
271    fold_constant_expression(expr, constant).filter(|folded| folded != expr)
272}
273
274fn fold_constant_expression(expr: &Expr, constant: Lit) -> Option<Expr> {
275    if let Expr::Atomic(_, Atom::Literal(existing)) = expr
276        && existing == &constant
277    {
278        return None;
279    }
280
281    if matches!(
282        (expr, constant.clone()),
283        (
284            Expr::Atomic(_, Atom::Reference(_)),
285            Lit::AbstractLiteral(AbstractLiteral::Matrix(_, _))
286        )
287    ) {
288        return None;
289    }
290
291    let folded = Expr::Atomic(Metadata::new(), Atom::Literal(constant));
292    if let Expr::TypeAnnotation(_, _, domain) = expr
293        && let Expr::Atomic(
294            _,
295            Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Matrix(elems, _))),
296        ) = &folded
297        && elems.is_empty()
298    {
299        return Some(Expr::TypeAnnotation(
300            Metadata::new(),
301            Moo::new(folded),
302            domain.clone(),
303        ));
304    }
305
306    if let Expr::DomainAnnotation(_, _, domain) = expr
307        && let Expr::Atomic(
308            _,
309            Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Matrix(elems, _))),
310        ) = &folded
311        && elems.is_empty()
312    {
313        return Some(Expr::DomainAnnotation(
314            Metadata::new(),
315            Moo::new(folded),
316            domain.clone(),
317        ));
318    }
319
320    if let Expr::Comprehension(_, comprehension) = expr
321        && let Expr::Atomic(
322            _,
323            Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Matrix(elems, _))),
324        ) = &folded
325        && elems.is_empty()
326        && let Some(domain) = comprehension.domain_of()
327    {
328        return Some(Expr::DomainAnnotation(
329            Metadata::new(),
330            Moo::new(folded),
331            domain,
332        ));
333    }
334
335    Some(folded)
336}
337
338fn has_only_locally_evaluable_operands(expr: &Expr) -> bool {
339    match expr {
340        Expr::Atomic(_, Atom::Literal(_)) => true,
341        // Whether a reference resolves to a constant is the evaluator's result, not an
342        // applicability question. Resolving it here would clone/evaluate the value letting and
343        // then make eval_constant do the same work again.
344        Expr::Atomic(_, Atom::Reference(_)) => true,
345        Expr::AbstractLiteral(_, lit) => abstract_literal_children_are_locally_evaluable(lit),
346        Expr::TypeAnnotation(_, inner, _) | Expr::DomainAnnotation(_, inner, _) => {
347            is_locally_evaluable_expr(inner.as_ref())
348        }
349        Expr::Comprehension(_, _) | Expr::Root(_, _) => false,
350        // Visit children by reference. `Uniplate::children()` clones the whole child list, so on a
351        // wide node (e.g. `or` over a large matrix) this test alone costs O(subtree); the evaluator
352        // hook runs it on every ancestor after every rewrite, which makes rewriting quadratic.
353        _ => {
354            let mut all_constant = true;
355            expr.for_each_expr_child(&mut |child| {
356                all_constant = all_constant && is_locally_evaluable_expr(child);
357            });
358            all_constant
359        }
360    }
361}
362
363fn is_locally_evaluable_expr(expr: &Expr) -> bool {
364    match expr {
365        Expr::Atomic(_, Atom::Literal(_)) => true,
366        Expr::Atomic(_, Atom::Reference(_)) => true,
367        Expr::AbstractLiteral(_, lit) => abstract_literal_children_are_locally_evaluable(lit),
368        Expr::TypeAnnotation(_, inner, _) | Expr::DomainAnnotation(_, inner, _) => {
369            is_locally_evaluable_expr(inner.as_ref())
370        }
371        _ => false,
372    }
373}
374
375fn abstract_literal_children_are_locally_evaluable(lit: &AbstractLiteral<Expr>) -> bool {
376    match lit {
377        AbstractLiteral::Set(items)
378        | AbstractLiteral::MSet(items)
379        | AbstractLiteral::Tuple(items)
380        | AbstractLiteral::Matrix(items, _) => items.iter().all(is_locally_evaluable_expr),
381        AbstractLiteral::Record(fields) => fields
382            .iter()
383            .all(|field| is_locally_evaluable_expr(&field.value)),
384        AbstractLiteral::Sequence(items) => items.iter().all(is_locally_evaluable_expr),
385        AbstractLiteral::Function(items) => items
386            .iter()
387            .all(|(from, to)| is_locally_evaluable_expr(from) && is_locally_evaluable_expr(to)),
388        AbstractLiteral::Relation(items) => items
389            .iter()
390            .all(|tuple| tuple.iter().all(is_locally_evaluable_expr)),
391        AbstractLiteral::Partition(parts) => parts
392            .iter()
393            .all(|part| part.iter().all(is_locally_evaluable_expr)),
394        AbstractLiteral::Permutation(cycles) => cycles
395            .iter()
396            .all(|cycle| cycle.iter().all(is_locally_evaluable_expr)),
397        AbstractLiteral::Variant(field) => is_locally_evaluable_expr(&field.value),
398    }
399}
400
401/// Simplify an expression to a constant if possible
402/// Returns:
403/// `None` if the expression cannot be simplified to a constant (e.g. if it contains a variable)
404/// `Some(Const)` if the expression can be simplified to a constant
405pub fn eval_constant(expr: &Expr) -> Option<Lit> {
406    match expr {
407        Expr::TypeAnnotation(_, expr, _) | Expr::DomainAnnotation(_, expr, _) => {
408            eval_constant(expr)
409        }
410        Expr::Supset(_, a, b) => {
411            let (
412                Lit::AbstractLiteral(AbstractLiteral::Set(a)),
413                Lit::AbstractLiteral(AbstractLiteral::Set(b)),
414            ) = (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?)
415            else {
416                return None;
417            };
418
419            let a_set: HashSet<Lit> = a.iter().cloned().collect();
420            let b_set: HashSet<Lit> = b.iter().cloned().collect();
421
422            if a_set.difference(&b_set).count() > 0 {
423                Some(Lit::Bool(a_set.is_superset(&b_set)))
424            } else {
425                Some(Lit::Bool(false))
426            }
427        }
428        Expr::SupsetEq(_, a, b) => {
429            let (
430                Lit::AbstractLiteral(AbstractLiteral::Set(a)),
431                Lit::AbstractLiteral(AbstractLiteral::Set(b)),
432            ) = (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?)
433            else {
434                return None;
435            };
436
437            Some(Lit::Bool(
438                a.iter()
439                    .cloned()
440                    .collect::<HashSet<Lit>>()
441                    .is_superset(&b.iter().cloned().collect::<HashSet<Lit>>()),
442            ))
443        }
444        Expr::Subset(_, a, b) => {
445            let (
446                Lit::AbstractLiteral(AbstractLiteral::Set(a)),
447                Lit::AbstractLiteral(AbstractLiteral::Set(b)),
448            ) = (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?)
449            else {
450                return None;
451            };
452
453            let a_set: HashSet<Lit> = a.iter().cloned().collect();
454            let b_set: HashSet<Lit> = b.iter().cloned().collect();
455
456            if b_set.difference(&a_set).count() > 0 {
457                Some(Lit::Bool(a_set.is_subset(&b_set)))
458            } else {
459                Some(Lit::Bool(false))
460            }
461        }
462        Expr::SubsetEq(_, a, b) => {
463            let (
464                Lit::AbstractLiteral(AbstractLiteral::Set(a)),
465                Lit::AbstractLiteral(AbstractLiteral::Set(b)),
466            ) = (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?)
467            else {
468                return None;
469            };
470
471            Some(Lit::Bool(
472                a.iter()
473                    .cloned()
474                    .collect::<HashSet<Lit>>()
475                    .is_subset(&b.iter().cloned().collect::<HashSet<Lit>>()),
476            ))
477        }
478        Expr::Intersect(_, a, b) => {
479            let (
480                Lit::AbstractLiteral(AbstractLiteral::Set(a)),
481                Lit::AbstractLiteral(AbstractLiteral::Set(b)),
482            ) = (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?)
483            else {
484                return None;
485            };
486
487            let mut res: Vec<Lit> = Vec::new();
488            for lit in a {
489                if b.contains(&lit) && !res.contains(&lit) {
490                    res.push(lit);
491                }
492            }
493            Some(Lit::AbstractLiteral(AbstractLiteral::Set(res)))
494        }
495        Expr::Difference(_, a, b) => {
496            let (
497                Lit::AbstractLiteral(AbstractLiteral::Set(a)),
498                Lit::AbstractLiteral(AbstractLiteral::Set(b)),
499            ) = (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?)
500            else {
501                return None;
502            };
503
504            let mut res: Vec<Lit> = Vec::new();
505            for lit in a {
506                if !b.contains(&lit) && !res.contains(&lit) {
507                    res.push(lit);
508                }
509            }
510            Some(Lit::AbstractLiteral(AbstractLiteral::Set(res)))
511        }
512        Expr::Union(_, a, b) => {
513            let (
514                Lit::AbstractLiteral(AbstractLiteral::Set(a)),
515                Lit::AbstractLiteral(AbstractLiteral::Set(b)),
516            ) = (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?)
517            else {
518                return None;
519            };
520
521            let mut res: Vec<Lit> = Vec::new();
522            for lit in a {
523                res.push(lit);
524            }
525            for lit in b {
526                if !res.contains(&lit) {
527                    res.push(lit);
528                }
529            }
530            Some(Lit::AbstractLiteral(AbstractLiteral::Set(res)))
531        }
532        Expr::In(_, a, b) => {
533            let member = eval_constant(a)?;
534            let collection = eval_constant(b)?;
535            let values = generator_values_from_constant_collection(&collection)?;
536            Some(Lit::Bool(values.iter().any(|value| {
537                value.essence_cmp(&member) == CmpOrdering::Equal
538            })))
539        }
540        Expr::FromSolution(_, _) => None,
541        Expr::DominanceRelation(_, _) => None,
542        Expr::InDomain(_, e, domain) => {
543            let Expr::Atomic(_, Atom::Literal(lit)) = e.as_ref() else {
544                return None;
545            };
546
547            domain.contains(lit).ok().map(Into::into)
548        }
549        Expr::Atomic(_, Atom::Literal(c)) => Some(c.clone()),
550        Expr::Atomic(_, Atom::Reference(reference)) => reference.resolve_constant(),
551        Expr::AbstractLiteral(_, a) => Some(Lit::AbstractLiteral(a.clone().into_literals()?)),
552        Expr::Comprehension(_, comprehension) => {
553            eval_constant_comprehension(comprehension.as_ref())
554        }
555        Expr::RecordField(_, rec, fld_name) => match eval_constant(rec.as_ref())? {
556            Lit::AbstractLiteral(AbstractLiteral::Record(ents)) => {
557                for Field { name, value } in ents {
558                    if name.eq(fld_name) {
559                        return Some(value);
560                    }
561                }
562                None
563            }
564            Lit::AbstractLiteral(AbstractLiteral::Variant(field)) if field.name == *fld_name => {
565                Some(field.value.clone())
566            }
567            _ => None,
568        },
569        Expr::UnsafeIndex(_, subject, indices) | Expr::SafeIndex(_, subject, indices) => {
570            let subject: Lit = eval_constant(subject.as_ref())?;
571            let indices: Vec<Lit> = indices
572                .iter()
573                .map(eval_constant)
574                .collect::<Option<Vec<Lit>>>()?;
575
576            match subject {
577                Lit::AbstractLiteral(subject @ AbstractLiteral::Matrix(_, _)) => {
578                    matrix::flatten_enumerate(subject)
579                        .find(|(i, _)| i == &indices)
580                        .map(|(_, x)| x)
581                }
582                Lit::AbstractLiteral(subject @ AbstractLiteral::Tuple(_)) => {
583                    let AbstractLiteral::Tuple(elems) = subject else {
584                        return None;
585                    };
586
587                    bug_assert!(indices.len() == 1, "nested tuples not supported yet");
588
589                    let Lit::Int(index) = indices[0].clone() else {
590                        return None;
591                    };
592
593                    if elems.len() < index as usize || index < 1 {
594                        return None;
595                    }
596
597                    // -1 for 0-indexing vs 1-indexing
598                    let item = elems[index as usize - 1].clone();
599
600                    Some(item)
601                }
602                Lit::AbstractLiteral(subject @ AbstractLiteral::Record(_)) => {
603                    let AbstractLiteral::Record(elems) = subject else {
604                        return None;
605                    };
606
607                    bug_assert!(indices.len() == 1, "nested record not supported yet");
608
609                    let Lit::Int(index) = indices[0].clone() else {
610                        return None;
611                    };
612
613                    if elems.len() < index as usize || index < 1 {
614                        return None;
615                    }
616
617                    // -1 for 0-indexing vs 1-indexing
618                    let item = elems[index as usize - 1].clone();
619                    Some(item.value)
620                }
621                _ => None,
622            }
623        }
624        Expr::UnsafeSlice(_, subject, indices) | Expr::SafeSlice(_, subject, indices) => {
625            let subject: Lit = eval_constant(subject.as_ref())?;
626            let Lit::AbstractLiteral(subject @ AbstractLiteral::Matrix(_, _)) = subject else {
627                return None;
628            };
629
630            let hole_dim = indices
631                .iter()
632                .cloned()
633                .position(|x| x.is_none())
634                .expect("slice expression should have a hole dimension");
635
636            let missing_domain = matrix::index_domains(&subject)[hole_dim].clone();
637
638            let indices: Vec<Option<Lit>> = indices
639                .iter()
640                .cloned()
641                .map(|x| {
642                    // the outer option represents success of this iterator, the inner the index
643                    // slice.
644                    match x {
645                        Some(x) => eval_constant(&x).map(Some),
646                        None => Some(None),
647                    }
648                })
649                .collect::<Option<Vec<Option<Lit>>>>()?;
650
651            let indices_in_slice: Vec<Vec<Lit>> = missing_domain
652                .values()
653                .ok()?
654                .map(|i| {
655                    let mut indices = indices.clone();
656                    indices[hole_dim] = Some(i);
657                    // These unwraps will only fail if we have multiple holes.
658                    // As this is invalid, panicking is fine.
659                    indices.into_iter().map(|x| x.unwrap()).collect_vec()
660                })
661                .collect_vec();
662
663            // Note: indices_in_slice is not necessarily sorted, so this is the best way.
664            let elems = matrix::flatten_enumerate(subject)
665                .filter(|(i, _)| indices_in_slice.contains(i))
666                .map(|(_, elem)| elem)
667                .collect();
668
669            Some(Lit::AbstractLiteral(into_matrix![elems]))
670        }
671        Expr::Abs(_, e) => un_op::<i32, i32>(|a| a.abs(), e).map(Lit::Int),
672        Expr::Eq(_, a, b) => Some(Lit::Bool(equal_constant_literals(
673            &eval_constant(a)?,
674            &eval_constant(b)?,
675        )?)),
676        Expr::Neq(_, a, b) => Some(Lit::Bool(!equal_constant_literals(
677            &eval_constant(a)?,
678            &eval_constant(b)?,
679        )?)),
680        Expr::Lt(_, a, b) => bin_op::<i32, bool>(|a, b| a < b, a, b).map(Lit::Bool),
681        Expr::Gt(_, a, b) => bin_op::<i32, bool>(|a, b| a > b, a, b).map(Lit::Bool),
682        Expr::Leq(_, a, b) => bin_op::<i32, bool>(|a, b| a <= b, a, b).map(Lit::Bool),
683        Expr::Geq(_, a, b) => bin_op::<i32, bool>(|a, b| a >= b, a, b).map(Lit::Bool),
684        Expr::Not(_, expr) => un_op::<bool, bool>(|e| !e, expr).map(Lit::Bool),
685        Expr::And(_, e) => {
686            vec_lit_op::<bool, bool>(|e| e.iter().all(|&e| e), e.as_ref()).map(Lit::Bool)
687        }
688        Expr::Table(_, _, _) => None,
689        Expr::NegativeTable(_, _, _) => None,
690        Expr::AtLeast(_, _, _, _) => None,
691        Expr::AtMost(_, _, _, _) => None,
692        Expr::Gcc(_, _, _, _) | Expr::GccWeak(_, _, _, _) => None,
693        Expr::Root(_, _) => None,
694        Expr::Or(_, es) => {
695            // possibly cheating; definitely should be in partial eval instead
696            for e in es.unwrap_list_cow()?.iter() {
697                if let Expr::Atomic(_, Atom::Literal(Lit::Bool(true))) = e {
698                    return Some(Lit::Bool(true));
699                };
700            }
701
702            vec_lit_op::<bool, bool>(|e| e.iter().any(|&e| e), es.as_ref()).map(Lit::Bool)
703        }
704        // A `catchUndef` still standing at constant-folding time has no bubble to consult, so the
705        // inner expression is total here and the default is unreachable.
706        Expr::CatchUndef(_, a, _) => eval_constant(a),
707        Expr::Imply(_, a, b) => bin_op::<bool, bool>(|a, b| !a || b, a, b).map(Lit::Bool),
708        Expr::Iff(_, a, b) => bin_op::<bool, bool>(|a, b| a == b, a, b).map(Lit::Bool),
709        Expr::Sum(_, exprs) => vec_lit_op::<i32, i32>(|e| e.iter().sum(), exprs).map(Lit::Int),
710        Expr::Product(_, exprs) => {
711            vec_lit_op::<i32, i32>(|e| e.iter().product(), exprs).map(Lit::Int)
712        }
713        Expr::FlatIneq(_, a, b, c) => {
714            let a: i32 = a.try_into().ok()?;
715            let b: i32 = b.try_into().ok()?;
716            let c: i32 = c.try_into().ok()?;
717
718            Some(Lit::Bool(a <= b + c))
719        }
720        Expr::FlatSumGeq(_, exprs, a) => {
721            let sum = exprs.iter().try_fold(0, |acc, atom: &Atom| {
722                let n: i32 = atom.try_into().ok()?;
723                let acc = acc + n;
724                Some(acc)
725            })?;
726
727            Some(Lit::Bool(sum >= a.try_into().ok()?))
728        }
729        Expr::FlatSumLeq(_, exprs, a) => {
730            let sum = exprs.iter().try_fold(0, |acc, atom: &Atom| {
731                let n: i32 = atom.try_into().ok()?;
732                let acc = acc + n;
733                Some(acc)
734            })?;
735
736            Some(Lit::Bool(sum >= a.try_into().ok()?))
737        }
738        Expr::FlatMinEq(_, vars, result) => {
739            let min = vars
740                .iter()
741                .try_fold(None, |acc: Option<i32>, atom: &Atom| {
742                    let n: i32 = atom.try_into().ok()?;
743                    Some(Some(acc.map_or(n, |m| m.min(n))))
744                })??;
745            let result: i32 = result.try_into().ok()?;
746            Some(Lit::Bool(min == result))
747        }
748        Expr::Min(_, e) => {
749            opt_vec_lit_op::<i32, i32>(|e| e.iter().min().copied(), e.as_ref()).map(Lit::Int)
750        }
751        Expr::Max(_, e) => {
752            opt_vec_lit_op::<i32, i32>(|e| e.iter().max().copied(), e.as_ref()).map(Lit::Int)
753        }
754        Expr::UnsafeDiv(_, a, b) | Expr::SafeDiv(_, a, b) => {
755            if unwrap_expr::<i32>(b)? == 0 {
756                return None;
757            }
758            bin_op::<i32, i32>(|a, b| ((a as f32) / (b as f32)).floor() as i32, a, b).map(Lit::Int)
759        }
760        Expr::UnsafeMod(_, a, b) | Expr::SafeMod(_, a, b) => {
761            if unwrap_expr::<i32>(b)? == 0 {
762                return None;
763            }
764            bin_op::<i32, i32>(|a, b| a - b * (a as f32 / b as f32).floor() as i32, a, b)
765                .map(Lit::Int)
766        }
767        Expr::Substring(_, s, t) => match (s.as_ref(), t.as_ref()) {
768            (
769                Expr::Atomic(_, Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Sequence(s)))),
770                Expr::Atomic(_, Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Sequence(t)))),
771            ) => {
772                if s.len() > t.len() {
773                    return Some(Lit::Bool(false));
774                }
775
776                let found = t.windows(s.len()).any(|window| window == s.as_slice());
777                Some(Lit::Bool(found))
778            }
779            _ => None,
780        },
781        Expr::Subsequence(_, s, t) => match (s.as_ref(), t.as_ref()) {
782            (
783                Expr::Atomic(_, Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Sequence(s)))),
784                Expr::Atomic(_, Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Sequence(t)))),
785            ) => {
786                let mut i = 0;
787                let mut j = 0;
788
789                while i < s.len() && j < t.len() {
790                    if s[i] == t[j] {
791                        i += 1;
792                    }
793                    j += 1;
794                }
795
796                Some(Lit::Bool(i == s.len()))
797            }
798            _ => None,
799        },
800        Expr::MinionDivEqUndefZero(_, a, b, c) => {
801            // div always rounds down
802            let a: i32 = a.try_into().ok()?;
803            let b: i32 = b.try_into().ok()?;
804            let c: i32 = c.try_into().ok()?;
805
806            if b == 0 {
807                return None;
808            }
809
810            let a = a as f32;
811            let b = b as f32;
812            let div: i32 = (a / b).floor() as i32;
813            Some(Lit::Bool(div == c))
814        }
815        Expr::Bubble(_, a, b) => bin_op::<bool, bool>(|a, b| a && b, a, b).map(Lit::Bool),
816        Expr::MinionReify(_, a, b) => {
817            let result = eval_constant(a)?;
818
819            let result: bool = result.try_into().ok()?;
820            let b: bool = b.try_into().ok()?;
821
822            Some(Lit::Bool(b == result))
823        }
824        Expr::MinionReifyImply(_, a, b) => {
825            let result = eval_constant(a)?;
826
827            let result: bool = result.try_into().ok()?;
828            let b: bool = b.try_into().ok()?;
829
830            if b {
831                Some(Lit::Bool(result))
832            } else {
833                Some(Lit::Bool(true))
834            }
835        }
836        Expr::MinionModuloEqUndefZero(_, a, b, c) => {
837            // From Savile Row. Same semantics as division.
838            //
839            //   a - (b * floor(a/b))
840            //
841            // We don't use % as it has the same semantics as /. We don't use / as we want to round
842            // down instead, not towards zero.
843
844            let a: i32 = a.try_into().ok()?;
845            let b: i32 = b.try_into().ok()?;
846            let c: i32 = c.try_into().ok()?;
847
848            if b == 0 {
849                return None;
850            }
851
852            let modulo = a - b * (a as f32 / b as f32).floor() as i32;
853            Some(Lit::Bool(modulo == c))
854        }
855        Expr::MinionPow(_, a, b, c) => {
856            // only available for positive a b c
857
858            let a: i32 = a.try_into().ok()?;
859            let b: i32 = b.try_into().ok()?;
860            let c: i32 = c.try_into().ok()?;
861
862            if a <= 0 {
863                return None;
864            }
865
866            if b <= 0 {
867                return None;
868            }
869
870            if c <= 0 {
871                return None;
872            }
873
874            Some(Lit::Bool(a ^ b == c))
875        }
876        Expr::MinionWInSet(_, _, _) => None,
877        Expr::MinionWInIntervalSet(_, x, intervals) => {
878            let x_lit: &Lit = x.try_into().ok()?;
879
880            let x_lit = match x_lit.clone() {
881                Lit::Int(i) => Some(i),
882                Lit::Bool(true) => Some(1),
883                Lit::Bool(false) => Some(0),
884                _ => None,
885            }?;
886
887            let mut intervals = intervals.iter();
888            while let Some(lower) = intervals.next() {
889                let Some(upper) = intervals.next() else {
890                    break;
891                };
892                if &x_lit >= lower && &x_lit <= upper {
893                    return Some(Lit::Bool(true));
894                }
895            }
896
897            Some(Lit::Bool(false))
898        }
899        Expr::Flatten(_, _, _) => {
900            // TODO
901            None
902        }
903        // Constant folding treats the SMT-native form exactly like the one it came from.
904        Expr::AllDiff(_, e) | Expr::SmtDistinct(_, e) => {
905            let es = e.unwrap_list_cow()?;
906            let mut lits: HashSet<Lit> = HashSet::new();
907            for expr in es.iter() {
908                let Expr::Atomic(_, Atom::Literal(x)) = expr else {
909                    return None;
910                };
911                match x {
912                    Lit::Int(_) | Lit::Bool(_) => {
913                        if lits.contains(x) {
914                            return Some(Lit::Bool(false));
915                        } else {
916                            lits.insert(x.clone());
917                        }
918                    }
919                    Lit::AbstractLiteral(_) => return None, // Reject AbstractLiteral cases
920                }
921            }
922            Some(Lit::Bool(true))
923        }
924        Expr::FlatAllDiff(_, es) => {
925            let mut lits: HashSet<Lit> = HashSet::new();
926            for atom in es {
927                let Atom::Literal(x) = atom else {
928                    return None;
929                };
930
931                match x {
932                    Lit::Int(_) | Lit::Bool(_) => {
933                        if lits.contains(x) {
934                            return Some(Lit::Bool(false));
935                        } else {
936                            lits.insert(x.clone());
937                        }
938                    }
939                    Lit::AbstractLiteral(_) => return None, // Reject AbstractLiteral cases
940                }
941            }
942            Some(Lit::Bool(true))
943        }
944        Expr::FlatWatchedLiteral(_, _, _) => None,
945        Expr::AuxDeclaration(_, _, _) => None,
946        Expr::Neg(_, a) => match eval_constant(a.as_ref())? {
947            Lit::Int(a) => Some(Lit::Int(-a)),
948            _ => None,
949        },
950        Expr::Factorial(_, a) => match eval_constant(a.as_ref())? {
951            Lit::Int(a) => factorial_i32(a).map(Lit::Int),
952            _ => None,
953        },
954        Expr::Minus(_, a, b) => bin_op::<i32, i32>(|a, b| a - b, a, b).map(Lit::Int),
955        Expr::FlatMinusEq(_, a, b) => {
956            let a: i32 = a.try_into().ok()?;
957            let b: i32 = b.try_into().ok()?;
958            Some(Lit::Bool(a == -b))
959        }
960        Expr::FlatProductEq(_, a, b, c) => {
961            let a: i32 = a.try_into().ok()?;
962            let b: i32 = b.try_into().ok()?;
963            let c: i32 = c.try_into().ok()?;
964            Some(Lit::Bool(a * b == c))
965        }
966        Expr::FlatWeightedSumLeq(_, cs, vs, total) => {
967            let cs: Vec<i32> = cs
968                .iter()
969                .map(|x| TryInto::<i32>::try_into(x).ok())
970                .collect::<Option<Vec<i32>>>()?;
971            let vs: Vec<i32> = vs
972                .iter()
973                .map(|x| TryInto::<i32>::try_into(x).ok())
974                .collect::<Option<Vec<i32>>>()?;
975            let total: i32 = total.try_into().ok()?;
976
977            let sum: i32 = izip!(cs, vs).fold(0, |acc, (c, v)| acc + (c * v));
978
979            Some(Lit::Bool(sum <= total))
980        }
981        Expr::FlatWeightedSumGeq(_, cs, vs, total) => {
982            let cs: Vec<i32> = cs
983                .iter()
984                .map(|x| TryInto::<i32>::try_into(x).ok())
985                .collect::<Option<Vec<i32>>>()?;
986            let vs: Vec<i32> = vs
987                .iter()
988                .map(|x| TryInto::<i32>::try_into(x).ok())
989                .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::FlatAbsEq(_, x, y) => {
997            let x: i32 = x.try_into().ok()?;
998            let y: i32 = y.try_into().ok()?;
999
1000            Some(Lit::Bool(x == y.abs()))
1001        }
1002        Expr::UnsafePow(_, a, b) | Expr::SafePow(_, a, b) => {
1003            let a: &Atom = a.try_into().ok()?;
1004            let a: i32 = a.try_into().ok()?;
1005
1006            let b: &Atom = b.try_into().ok()?;
1007            let b: i32 = b.try_into().ok()?;
1008
1009            if (a != 0 || b != 0) && b >= 0 {
1010                Some(Lit::Int(a.pow(b as u32)))
1011            } else {
1012                None
1013            }
1014        }
1015        Expr::Metavar(_, _) => None,
1016        Expr::MinionElementOne(_, _, _, _) => None,
1017        Expr::ToInt(_, expression) => {
1018            let lit = eval_constant(expression.as_ref())?;
1019            match lit {
1020                Lit::Int(_) => Some(lit),
1021                Lit::Bool(true) => Some(Lit::Int(1)),
1022                Lit::Bool(false) => Some(Lit::Int(0)),
1023                _ => None,
1024            }
1025        }
1026        Expr::SATInt(_, _, _, _) => {
1027            // TODO: If this SATInt is composed of literals, we should evaluate it back to an
1028            // integer literal.
1029            //
1030            // This is important because `is_all_constant` currently returns true for SATInts
1031            // containing no references. If we don't evaluate them here, bubble rules will skip
1032            // them (thinking they'll be constant-folded later), but they'll actually reach
1033            // the solver adaptors as un-encoded unsafe operations, causing panics.
1034            None
1035        }
1036        Expr::PairwiseSum(_, a, b) => {
1037            match (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?) {
1038                (Lit::Int(a_int), Lit::Int(b_int)) => Some(Lit::Int(a_int + b_int)),
1039                _ => None,
1040            }
1041        }
1042        Expr::PairwiseProduct(_, a, b) => {
1043            match (eval_constant(a.as_ref())?, eval_constant(b.as_ref())?) {
1044                (Lit::Int(a_int), Lit::Int(b_int)) => Some(Lit::Int(a_int * b_int)),
1045                _ => None,
1046            }
1047        }
1048        Expr::Defined(_, f) => {
1049            let Lit::AbstractLiteral(AbstractLiteral::Function(pairs)) = eval_constant(f)? else {
1050                return None;
1051            };
1052            Some(Lit::AbstractLiteral(AbstractLiteral::Set(
1053                pairs.into_iter().map(|(key, _)| key).collect(),
1054            )))
1055        }
1056        Expr::Range(_, f) => {
1057            let Lit::AbstractLiteral(AbstractLiteral::Function(pairs)) = eval_constant(f)? else {
1058                return None;
1059            };
1060            let mut values = Vec::new();
1061            for (_, value) in pairs {
1062                if !values.contains(&value) {
1063                    values.push(value);
1064                }
1065            }
1066            Some(Lit::AbstractLiteral(AbstractLiteral::Set(values)))
1067        }
1068        Expr::Image(_, f, arg) => {
1069            let arg = eval_constant(arg)?;
1070            match eval_constant(f)? {
1071                Lit::AbstractLiteral(AbstractLiteral::Function(pairs)) => pairs
1072                    .into_iter()
1073                    .find(|(key, _)| *key == arg)
1074                    .map(|(_, value)| value),
1075                // A sequence is a function from int(1..n), so applying it reads the entry at
1076                // that one-based position. Out of range is undefined, so unevaluable.
1077                Lit::AbstractLiteral(AbstractLiteral::Sequence(values)) => {
1078                    let Lit::Int(index) = arg else {
1079                        return None;
1080                    };
1081                    usize::try_from(index)
1082                        .ok()
1083                        .filter(|index| *index >= 1)
1084                        .and_then(|index| values.get(index - 1).cloned())
1085                }
1086                // Cycle notation: find which cycle (if any) mentions `arg` and return the next
1087                // element in it (wrapping around); an element mentioned in no cycle is an
1088                // implicit fixed point, mapping to itself.
1089                Lit::AbstractLiteral(AbstractLiteral::Permutation(cycles)) => {
1090                    for cycle in &cycles {
1091                        if let Some(pos) = cycle.iter().position(|x| *x == arg) {
1092                            return Some(cycle[(pos + 1) % cycle.len()].clone());
1093                        }
1094                    }
1095                    Some(arg)
1096                }
1097                _ => None,
1098            }
1099        }
1100        Expr::PreImage(_, f, img) => {
1101            let Lit::AbstractLiteral(AbstractLiteral::Function(pairs)) = eval_constant(f)? else {
1102                return None;
1103            };
1104            let img = eval_constant(img)?;
1105            let mut keys = Vec::new();
1106            for (key, value) in pairs {
1107                if value == img && !keys.contains(&key) {
1108                    keys.push(key);
1109                }
1110            }
1111            Some(Lit::AbstractLiteral(AbstractLiteral::Set(keys)))
1112        }
1113        // Not yet needed by any in-scope function case; the partial evaluator already refuses
1114        // these gracefully (Err(RuleNotApplicable)) rather than panicking.
1115        Expr::ImageSet(_, _, _) => None,
1116        Expr::Inverse(_, _, _) => None,
1117        Expr::PermInverse(_, _) => None,
1118        Expr::Compose(_, _, _) => None,
1119        Expr::Restrict(_, _, _) => None,
1120        Expr::ToSet(_, _) => None,
1121        Expr::ToMSet(_, _) => None,
1122        Expr::ToRelation(_, _) => None,
1123        Expr::Active(_, variant, alternative) => {
1124            let Lit::AbstractLiteral(AbstractLiteral::Variant(field)) =
1125                eval_constant(variant.as_ref())?
1126            else {
1127                return None;
1128            };
1129            Some(Lit::Bool(field.name == *alternative))
1130        }
1131        Expr::RelationProj(_, _, _) => todo!(),
1132        // Not yet needed by any in-scope partition case; the partial evaluator already refuses
1133        // these gracefully (Err(RuleNotApplicable)) rather than panicking. Constant partition
1134        // literal equality/membership already works via the generic Literal equality path above,
1135        // without going through these operators at all.
1136        Expr::Apart(_, _, _) => None,
1137        Expr::Together(_, _, _) => None,
1138        Expr::Participants(_, _) => None,
1139        Expr::Party(_, _, _) => None,
1140        Expr::Parts(_, _) => None,
1141        Expr::Card(_, collection) => {
1142            let Lit::AbstractLiteral(collection) = eval_constant(collection)? else {
1143                return None;
1144            };
1145            let length = match collection {
1146                AbstractLiteral::Set(values)
1147                | AbstractLiteral::MSet(values)
1148                | AbstractLiteral::Sequence(values)
1149                | AbstractLiteral::Matrix(values, _) => values.len(),
1150                AbstractLiteral::Function(entries) => entries.len(),
1151                AbstractLiteral::Relation(entries) => entries.len(),
1152                // A permutation's cardinality is its numMoved count -- how many elements are
1153                // mentioned in some cycle -- not the number of cycles, matching the same
1154                // "unmentioned = fixed point" convention used everywhere else for permutations
1155                // (contains()'s size check, PermutationAsFunction's own numMoved structural
1156                // constraint).
1157                AbstractLiteral::Permutation(cycles) => cycles.iter().flatten().count(),
1158                _ => return None,
1159            };
1160            i32::try_from(length).ok().map(Lit::Int)
1161        }
1162        Expr::LexLt(_, a, b) => {
1163            let lt = vec_expr_pairs_op::<i32, _>(a, b, |pairs, (a_len, b_len)| {
1164                pairs
1165                    .iter()
1166                    .find_map(|(a, b)| match a.cmp(b) {
1167                        CmpOrdering::Less => Some(true),     // First difference is <
1168                        CmpOrdering::Greater => Some(false), // First difference is >
1169                        CmpOrdering::Equal => None,          // No difference
1170                    })
1171                    .unwrap_or(a_len < b_len) // [1,1] <lex [1,1,x]
1172            })?;
1173            Some(lt.into())
1174        }
1175        Expr::LexLeq(_, a, b) => {
1176            let lt = vec_expr_pairs_op::<i32, _>(a, b, |pairs, (a_len, b_len)| {
1177                pairs
1178                    .iter()
1179                    .find_map(|(a, b)| match a.cmp(b) {
1180                        CmpOrdering::Less => Some(true),
1181                        CmpOrdering::Greater => Some(false),
1182                        CmpOrdering::Equal => None,
1183                    })
1184                    .unwrap_or(a_len <= b_len) // [1,1] <=lex [1,1,x]
1185            })?;
1186            Some(lt.into())
1187        }
1188        Expr::LexGt(_, a, b) => eval_constant(&Expr::LexLt(Metadata::new(), b.clone(), a.clone())),
1189        Expr::LexGeq(_, a, b) => {
1190            eval_constant(&Expr::LexLeq(Metadata::new(), b.clone(), a.clone()))
1191        }
1192        Expr::FlatLexLt(_, a, b) => {
1193            let lt = atoms_pairs_op::<i32, _>(a, b, |pairs, (a_len, b_len)| {
1194                pairs
1195                    .iter()
1196                    .find_map(|(a, b)| match a.cmp(b) {
1197                        CmpOrdering::Less => Some(true),
1198                        CmpOrdering::Greater => Some(false),
1199                        CmpOrdering::Equal => None,
1200                    })
1201                    .unwrap_or(a_len < b_len)
1202            })?;
1203            Some(lt.into())
1204        }
1205        Expr::FlatLexLeq(_, a, b) => {
1206            let lt = atoms_pairs_op::<i32, _>(a, b, |pairs, (a_len, b_len)| {
1207                pairs
1208                    .iter()
1209                    .find_map(|(a, b)| match a.cmp(b) {
1210                        CmpOrdering::Less => Some(true),
1211                        CmpOrdering::Greater => Some(false),
1212                        CmpOrdering::Equal => None,
1213                    })
1214                    .unwrap_or(a_len <= b_len)
1215            })?;
1216            Some(lt.into())
1217        }
1218        Expr::AllDifferentExcept(_, _, _) | Expr::ElementId(_, _, _) => None,
1219        // Needs the target's domain to expand, which this function has no access to; the
1220        // fallback rewrite rule (passes::attribute_as_constraint) handles expansion instead.
1221        Expr::AttributeAsConstraint(_, _, _, _) => None,
1222    }
1223}
1224
1225pub fn un_op<T, A>(f: fn(T) -> A, a: &Expr) -> Option<A>
1226where
1227    T: TryFrom<Lit>,
1228{
1229    let a = unwrap_expr::<T>(a)?;
1230    Some(f(a))
1231}
1232
1233pub fn bin_op<T, A>(f: fn(T, T) -> A, a: &Expr, b: &Expr) -> Option<A>
1234where
1235    T: TryFrom<Lit>,
1236{
1237    let a = unwrap_expr::<T>(a)?;
1238    let b = unwrap_expr::<T>(b)?;
1239    Some(f(a, b))
1240}
1241
1242#[allow(dead_code)]
1243pub fn tern_op<T, A>(f: fn(T, T, T) -> A, a: &Expr, b: &Expr, c: &Expr) -> Option<A>
1244where
1245    T: TryFrom<Lit>,
1246{
1247    let a = unwrap_expr::<T>(a)?;
1248    let b = unwrap_expr::<T>(b)?;
1249    let c = unwrap_expr::<T>(c)?;
1250    Some(f(a, b, c))
1251}
1252
1253pub fn vec_op<T, A>(f: fn(Vec<T>) -> A, a: &[Expr]) -> Option<A>
1254where
1255    T: TryFrom<Lit>,
1256{
1257    let a = a.iter().map(unwrap_expr).collect::<Option<Vec<T>>>()?;
1258    Some(f(a))
1259}
1260
1261pub fn vec_lit_op<T, A>(f: fn(Vec<T>) -> A, a: &Expr) -> Option<A>
1262where
1263    T: TryFrom<Lit>,
1264{
1265    Some(f(eval_list_items(a)?))
1266}
1267
1268type PairsCallback<T, A> = fn(Vec<(T, T)>, (usize, usize)) -> A;
1269
1270/// Calls the given function on each consecutive pair of elements in the list expressions.
1271/// Also passes the length of the two lists.
1272fn vec_expr_pairs_op<T, A>(a: &Expr, b: &Expr, f: PairsCallback<T, A>) -> Option<A>
1273where
1274    T: TryFrom<Lit>,
1275{
1276    let a_exprs = a.clone().unwrap_matrix_unchecked()?.0;
1277    let b_exprs = b.clone().unwrap_matrix_unchecked()?.0;
1278    let lens = (a_exprs.len(), b_exprs.len());
1279
1280    let lit_pairs = std::iter::zip(a_exprs, b_exprs)
1281        .map(|(a, b)| Some((unwrap_expr(&a)?, unwrap_expr(&b)?)))
1282        .collect::<Option<Vec<(T, T)>>>()?;
1283    Some(f(lit_pairs, lens))
1284}
1285
1286/// Same as [`vec_expr_pairs_op`], but over slices of atoms.
1287fn atoms_pairs_op<T, A>(a: &[Atom], b: &[Atom], f: PairsCallback<T, A>) -> Option<A>
1288where
1289    T: TryFrom<Atom>,
1290{
1291    let lit_pairs = Iterator::zip(a.iter(), b.iter())
1292        .map(|(a, b)| Some((a.clone().try_into().ok()?, b.clone().try_into().ok()?)))
1293        .collect::<Option<Vec<(T, T)>>>()?;
1294    Some(f(lit_pairs, (a.len(), b.len())))
1295}
1296
1297pub fn opt_vec_op<T, A>(f: fn(Vec<T>) -> Option<A>, a: &[Expr]) -> Option<A>
1298where
1299    T: TryFrom<Lit>,
1300{
1301    let a = a.iter().map(unwrap_expr).collect::<Option<Vec<T>>>()?;
1302    f(a)
1303}
1304
1305pub fn opt_vec_lit_op<T, A>(f: fn(Vec<T>) -> Option<A>, a: &Expr) -> Option<A>
1306where
1307    T: TryFrom<Lit>,
1308{
1309    f(eval_list_items(a)?)
1310}
1311
1312#[allow(dead_code)]
1313pub fn flat_op<T, A>(f: fn(Vec<T>, T) -> A, a: &[Expr], b: &Expr) -> Option<A>
1314where
1315    T: TryFrom<Lit>,
1316{
1317    let a = a.iter().map(unwrap_expr).collect::<Option<Vec<T>>>()?;
1318    let b = unwrap_expr::<T>(b)?;
1319    Some(f(a, b))
1320}
1321
1322pub fn unwrap_expr<T: TryFrom<Lit>>(expr: &Expr) -> Option<T> {
1323    let c = eval_constant(expr)?;
1324    TryInto::<T>::try_into(c).ok()
1325}
1326
1327fn eval_list_items<T>(expr: &Expr) -> Option<Vec<T>>
1328where
1329    T: TryFrom<Lit>,
1330{
1331    if let Some((items, _)) = expr.unwrap_matrix_unchecked_ref() {
1332        return items.iter().map(unwrap_expr).collect();
1333    }
1334
1335    let collection = eval_constant(expr)?;
1336    generator_values_from_constant_collection(&collection)?
1337        .into_iter()
1338        .map(TryInto::try_into)
1339        .collect::<Result<Vec<_>, _>>()
1340        .ok()
1341}
1342
1343fn eval_constant_comprehension(comprehension: &Comprehension) -> Option<Lit> {
1344    let mut values = Vec::new();
1345    eval_comprehension_qualifiers(comprehension, 0, &mut values)?;
1346    Some(Lit::AbstractLiteral(
1347        AbstractLiteral::matrix_implied_indices(values),
1348    ))
1349}
1350
1351fn eval_comprehension_qualifiers(
1352    comprehension: &Comprehension,
1353    qualifier_index: usize,
1354    values: &mut Vec<Lit>,
1355) -> Option<()> {
1356    if qualifier_index == comprehension.qualifiers.len() {
1357        values.push(eval_constant(&comprehension.return_expression)?);
1358        return Some(());
1359    }
1360
1361    match &comprehension.qualifiers[qualifier_index] {
1362        ComprehensionQualifier::Generator { ptr } => {
1363            let domain = ptr.domain()?;
1364            let generator_values = domain
1365                .resolve()
1366                .and_then(|x| x.values())
1367                .ok()?
1368                .collect_vec();
1369
1370            for value in generator_values {
1371                with_temporary_quantified_binding(ptr, &value, || {
1372                    eval_comprehension_qualifiers(comprehension, qualifier_index + 1, values)
1373                })?;
1374            }
1375        }
1376        ComprehensionQualifier::ExpressionGenerator { ptr } => {
1377            // clone immediately so the read lock guard is dropped
1378            let expr = ptr.as_quantified_expr()?.clone();
1379            let generator_values = generator_values_from_expr(&expr)?;
1380
1381            for value in generator_values {
1382                with_temporary_quantified_binding(ptr, &value, || {
1383                    eval_comprehension_qualifiers(comprehension, qualifier_index + 1, values)
1384                })?;
1385            }
1386        }
1387        ComprehensionQualifier::Condition(condition) => match eval_constant(condition)? {
1388            Lit::Bool(true) => {
1389                eval_comprehension_qualifiers(comprehension, qualifier_index + 1, values)?
1390            }
1391            Lit::Bool(false) => {}
1392            _ => return None,
1393        },
1394    }
1395
1396    Some(())
1397}
1398
1399/// Values for a constant collection expression used during constant folding.
1400///
1401/// This does not enumerate decision-variable domains; quantification over decisions is not
1402/// unrolled here.
1403pub fn generator_values_from_expr(expr: &Expr) -> Option<Vec<Lit>> {
1404    generator_values_from_constant_collection(&eval_constant(expr)?)
1405}
1406
1407pub(crate) fn generator_values_from_constant_collection(lit: &Lit) -> Option<Vec<Lit>> {
1408    match lit {
1409        Lit::AbstractLiteral(AbstractLiteral::Set(values))
1410        | Lit::AbstractLiteral(AbstractLiteral::MSet(values))
1411        | Lit::AbstractLiteral(AbstractLiteral::Tuple(values)) => Some(values.clone()),
1412        Lit::AbstractLiteral(AbstractLiteral::Matrix(values, _)) => Some(values.clone()),
1413        Lit::AbstractLiteral(list) => list.unwrap_list().cloned(),
1414        _ => None,
1415    }
1416}
1417
1418fn with_temporary_quantified_binding<T>(
1419    quantified: &crate::ast::DeclarationPtr,
1420    value: &Lit,
1421    f: impl FnOnce() -> Option<T>,
1422) -> Option<T> {
1423    let mut targets = vec![quantified.clone()];
1424    if let DeclarationKind::Quantified(inner) = &*quantified.kind()
1425        && let Some(generator) = inner.generator()
1426    {
1427        targets.push(generator.clone());
1428    }
1429
1430    let mut originals = Vec::with_capacity(targets.len());
1431    for mut target in targets {
1432        let old_kind = target.replace_kind(DeclarationKind::TemporaryValueLetting(Expr::Atomic(
1433            Metadata::new(),
1434            Atom::Literal(value.clone()),
1435        )));
1436        originals.push((target, old_kind));
1437    }
1438
1439    let result = f();
1440
1441    for (mut target, old_kind) in originals.into_iter().rev() {
1442        let _ = target.replace_kind(old_kind);
1443    }
1444
1445    result
1446}
1447
1448#[cfg(test)]
1449mod tests {
1450    use super::*;
1451    use crate::matrix_expr;
1452
1453    fn int_lit(value: i32) -> Expr {
1454        Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Int(value)))
1455    }
1456
1457    fn bool_lit(value: bool) -> Expr {
1458        Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Bool(value)))
1459    }
1460
1461    fn variant_lit(alternative: &str, value: Expr) -> Expr {
1462        Expr::AbstractLiteral(
1463            Metadata::new(),
1464            AbstractLiteral::Variant(Moo::new(Field {
1465                name: crate::ast::Name::user(alternative),
1466                value,
1467            })),
1468        )
1469    }
1470
1471    fn root(exprs: Vec<Expr>) -> Expr {
1472        Expr::Root(Metadata::new(), exprs)
1473    }
1474
1475    #[test]
1476    fn evaluates_active_on_variant_literals() {
1477        let variant = Moo::new(variant_lit("value", int_lit(2)));
1478        let active = Expr::Active(
1479            Metadata::new(),
1480            variant.clone(),
1481            crate::ast::Name::user("value"),
1482        );
1483        let inactive = Expr::Active(Metadata::new(), variant, crate::ast::Name::user("flag"));
1484
1485        assert_eq!(eval_constant(&active), Some(Lit::Bool(true)));
1486        assert_eq!(eval_constant(&inactive), Some(Lit::Bool(false)));
1487        assert!(run_partial_evaluator_local(&active).is_ok());
1488
1489        let field = Expr::RecordField(
1490            Metadata::new(),
1491            Moo::new(variant_lit("value", int_lit(2))),
1492            crate::ast::Name::user("value"),
1493        );
1494        assert_eq!(eval_constant(&field), Some(Lit::Int(2)));
1495    }
1496
1497    #[test]
1498    fn local_root_partial_eval_strips_true_constraints() {
1499        let expr = root(vec![bool_lit(true), int_lit(1)]);
1500        let Expr::Root(_, exprs) = &expr else {
1501            panic!("expected root");
1502        };
1503        let normalised = normalise_root_constraints_local(exprs).unwrap();
1504        assert_eq!(normalised, root(vec![int_lit(1)]));
1505    }
1506
1507    #[test]
1508    fn local_root_partial_eval_propagates_false() {
1509        let expr = root(vec![bool_lit(false), int_lit(1)]);
1510        let Expr::Root(_, exprs) = &expr else {
1511            panic!("expected root");
1512        };
1513        let normalised = normalise_root_constraints_local(exprs).unwrap();
1514        assert_eq!(normalised, root(vec![bool_lit(false)]));
1515    }
1516
1517    #[test]
1518    fn deep_root_normalisation_folds_ground_constraint() {
1519        let expr = root(vec![Expr::Sum(
1520            Metadata::new(),
1521            Moo::new(matrix_expr![int_lit(1), int_lit(2), int_lit(3)]),
1522        )]);
1523        let normalised = normalise_root_constraints_deep(&expr).unwrap();
1524        assert_eq!(normalised, root(vec![int_lit(6)]));
1525    }
1526
1527    #[test]
1528    fn deep_root_normalisation_applies_partial_eval_steps() {
1529        let expr = root(vec![Expr::Or(
1530            Metadata::new(),
1531            Moo::new(matrix_expr![bool_lit(false), int_lit(1)]),
1532        )]);
1533        let normalised = normalise_root_constraints_deep(&expr).unwrap();
1534        assert_eq!(
1535            normalised,
1536            root(vec![Expr::Or(
1537                Metadata::new(),
1538                Moo::new(matrix_expr![int_lit(1)]),
1539            )])
1540        );
1541    }
1542
1543    #[test]
1544    fn selective_deep_root_normalisation_skips_solver_flat_constraints() {
1545        let flat = Expr::FlatProductEq(
1546            Metadata::new(),
1547            Moo::new(Atom::Literal(Lit::Int(1))),
1548            Moo::new(Atom::Literal(Lit::Int(2))),
1549            Moo::new(Atom::Literal(Lit::Int(3))),
1550        );
1551        let expr = root(vec![bool_lit(true), flat]);
1552        assert!(normalise_root_constraints_deep(&expr).is_none());
1553    }
1554
1555    #[test]
1556    fn deep_root_normalisation_terminates_on_already_folded_constraint() {
1557        let expr = root(vec![int_lit(5)]);
1558        assert!(normalise_root_constraints_deep(&expr).is_none());
1559    }
1560
1561    #[test]
1562    fn local_evaluator_normalisation_terminates_on_already_folded_literal() {
1563        let expr = int_lit(5);
1564        assert!(normalise_evaluator_local(&expr).is_none());
1565    }
1566
1567    #[test]
1568    fn constant_set_cardinality_is_folded() {
1569        let set = Expr::Atomic(
1570            Metadata::new(),
1571            Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Set(vec![
1572                Lit::Int(1),
1573                Lit::Int(2),
1574            ]))),
1575        );
1576        let cardinality = Expr::Card(Metadata::new(), Moo::new(set));
1577
1578        assert_eq!(eval_constant(&cardinality), Some(Lit::Int(2)));
1579    }
1580
1581    #[test]
1582    fn constant_set_minimum_and_maximum_are_folded() {
1583        let set = Expr::from(Lit::AbstractLiteral(AbstractLiteral::Set(vec![
1584            Lit::Int(4),
1585            Lit::Int(1),
1586            Lit::Int(3),
1587        ])));
1588
1589        assert_eq!(
1590            eval_constant(&Expr::Min(Metadata::new(), Moo::new(set.clone()))),
1591            Some(Lit::Int(1))
1592        );
1593        assert_eq!(
1594            eval_constant(&Expr::Max(Metadata::new(), Moo::new(set))),
1595            Some(Lit::Int(4))
1596        );
1597    }
1598
1599    #[test]
1600    fn constant_set_membership_supports_composite_values() {
1601        let member =
1602            Lit::AbstractLiteral(AbstractLiteral::Tuple(vec![Lit::Bool(true), Lit::Int(2)]));
1603        let set = Lit::AbstractLiteral(AbstractLiteral::Set(vec![member.clone()]));
1604        let membership = Expr::In(
1605            Metadata::new(),
1606            Moo::new(Expr::from(member)),
1607            Moo::new(Expr::from(set)),
1608        );
1609
1610        assert_eq!(eval_constant(&membership), Some(Lit::Bool(true)));
1611    }
1612
1613    #[test]
1614    fn constant_equality_supports_composite_values() {
1615        let tuple = Lit::AbstractLiteral(AbstractLiteral::Tuple(vec![Lit::Int(1), Lit::Int(3)]));
1616        let equal = Expr::Eq(
1617            Metadata::new(),
1618            Moo::new(Expr::from(tuple.clone())),
1619            Moo::new(Expr::from(tuple)),
1620        );
1621        let unequal = Expr::Neq(
1622            Metadata::new(),
1623            Moo::new(Expr::from(Lit::AbstractLiteral(AbstractLiteral::Tuple(
1624                vec![Lit::Int(1), Lit::Int(3)],
1625            )))),
1626            Moo::new(Expr::from(Lit::AbstractLiteral(AbstractLiteral::Tuple(
1627                vec![Lit::Int(1), Lit::Int(4)],
1628            )))),
1629        );
1630
1631        assert_eq!(eval_constant(&equal), Some(Lit::Bool(true)));
1632        assert_eq!(eval_constant(&unequal), Some(Lit::Bool(true)));
1633
1634        let set_equality = Expr::Eq(
1635            Metadata::new(),
1636            Moo::new(Expr::from(Lit::AbstractLiteral(AbstractLiteral::Set(
1637                vec![Lit::Int(1), Lit::Int(2)],
1638            )))),
1639            Moo::new(Expr::from(Lit::AbstractLiteral(AbstractLiteral::Set(
1640                vec![Lit::Int(2), Lit::Int(1)],
1641            )))),
1642        );
1643        assert_eq!(eval_constant(&set_equality), Some(Lit::Bool(true)));
1644    }
1645
1646    #[test]
1647    fn finish_root_normalisation_applies_local_root_rules() {
1648        let expr = root(vec![bool_lit(true), int_lit(1)]);
1649        let normalised = finish_root_evaluator_normalisation(&expr).unwrap();
1650        assert_eq!(normalised, root(vec![int_lit(1)]));
1651    }
1652
1653    #[test]
1654    fn selective_deep_only_constraint_folds_target_index() {
1655        let ground = Expr::Sum(
1656            Metadata::new(),
1657            Moo::new(matrix_expr![int_lit(1), int_lit(2), int_lit(3)]),
1658        );
1659        let untouched = Expr::Sum(
1660            Metadata::new(),
1661            Moo::new(matrix_expr![int_lit(4), int_lit(5)]),
1662        );
1663        let expr = root(vec![ground, untouched.clone()]);
1664        let normalised = normalise_root_selective_deep_expr(&expr, Some(0)).unwrap();
1665        assert_eq!(normalised, root(vec![int_lit(6), untouched]));
1666    }
1667
1668    #[test]
1669    fn finish_root_normalisation_flattens_top_level_and() {
1670        let expr = root(vec![Expr::And(
1671            Metadata::new(),
1672            Moo::new(matrix_expr![bool_lit(true), int_lit(1), int_lit(2)]),
1673        )]);
1674        let normalised = finish_root_evaluator_normalisation(&expr).unwrap();
1675        assert_eq!(normalised, root(vec![int_lit(1), int_lit(2)]));
1676    }
1677
1678    fn permutation_lit(cycles: Vec<Vec<Expr>>) -> Expr {
1679        Expr::AbstractLiteral(Metadata::new(), AbstractLiteral::Permutation(cycles))
1680    }
1681
1682    #[test]
1683    fn evaluates_image_on_a_permutation_literal_moved_point() {
1684        let p = Moo::new(permutation_lit(vec![
1685            vec![int_lit(1), int_lit(3), int_lit(5)],
1686            vec![int_lit(2), int_lit(4)],
1687        ]));
1688        let image_1 = Expr::Image(Metadata::new(), p.clone(), Moo::new(int_lit(1)));
1689        let image_3 = Expr::Image(Metadata::new(), p.clone(), Moo::new(int_lit(3)));
1690        let image_5 = Expr::Image(Metadata::new(), p.clone(), Moo::new(int_lit(5)));
1691        let image_2 = Expr::Image(Metadata::new(), p.clone(), Moo::new(int_lit(2)));
1692        let image_4 = Expr::Image(Metadata::new(), p, Moo::new(int_lit(4)));
1693
1694        assert_eq!(eval_constant(&image_1), Some(Lit::Int(3)));
1695        assert_eq!(eval_constant(&image_3), Some(Lit::Int(5)));
1696        assert_eq!(eval_constant(&image_5), Some(Lit::Int(1)));
1697        assert_eq!(eval_constant(&image_2), Some(Lit::Int(4)));
1698        assert_eq!(eval_constant(&image_4), Some(Lit::Int(2)));
1699    }
1700
1701    #[test]
1702    fn evaluates_image_on_a_permutation_literal_fixed_point() {
1703        let p = Moo::new(permutation_lit(vec![vec![int_lit(1), int_lit(2)]]));
1704        let image_3 = Expr::Image(Metadata::new(), p, Moo::new(int_lit(3)));
1705
1706        // 3 is not mentioned in any cycle, so it is an implicit fixed point.
1707        assert_eq!(eval_constant(&image_3), Some(Lit::Int(3)));
1708    }
1709}