1
mod lex;
2
mod normalise;
3
mod set;
4
mod short_table;
5

            
6
/************************************************************************/
7
/*        Rules for translating to Minion-supported constraints         */
8
/************************************************************************/
9

            
10
use std::{
11
    collections::{HashMap, HashSet},
12
    convert::TryInto,
13
};
14

            
15
use crate::types::matrix::MatrixComponents;
16
use conjure_cp::representation::ReprRule;
17

            
18
use crate::extra_check;
19
use crate::shared::utils::{
20
    defer_aux_var, flatten_children_to_aux_vars, is_flat, rewrite_children, to_aux_var_in,
21
};
22
use crate::types::matrix::try_index_matrix_components;
23
use conjure_cp::ast::categories::{Category, CategoryOf};
24
use conjure_cp::ast::{Domain, DomainPtr, GroundDomain, HasDomain, Moo, eval_constant};
25
use conjure_cp::{
26
    ast::Metadata,
27
    ast::{
28
        AbstractLiteral, Atom, Expression as Expr, Literal as Lit, Name, Range, Reference,
29
        ReturnType, SymbolTable, Typeable,
30
    },
31
    into_matrix_expr, matrix_expr,
32
    rule_engine::{
33
        ApplicationError, ApplicationResult, RuleEffect, register_rule, register_rule_set,
34
    },
35
    settings::SolverFamily,
36
};
37

            
38
use ApplicationError::RuleNotApplicable;
39
use itertools::Itertools;
40

            
41
register_rule_set!("Minion", ("Base"), |f: &SolverFamily| {
42
294803
    matches!(f, SolverFamily::Minion)
43
294803
});
44

            
45
/// Inlines constant matrix references just for Minion so Base matrix rules can lower them
46
/// without affecting other backends.
47
#[register_rule("Minion", 9000, [SafeIndex])]
48
509684
fn inline_constant_matrix_subject_for_minion(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
49
509684
    let Expr::SafeIndex(_, subject, indices) = expr else {
50
5634
        return Err(RuleNotApplicable);
51
    };
52

            
53
504050
    let Expr::Atomic(_, Atom::Reference(reference)) = subject.as_ref() else {
54
43543
        return Err(RuleNotApplicable);
55
    };
56

            
57
    // When all but the leading index are fixed, project those dimensions while the value letting
58
    // is still borrowed. The generic matrix rules would otherwise clone the complete matrix and
59
    // build one SafeIndex per outer element, only to evaluate every one of them independently.
60
460507
    if let Some(projected) = reference
61
21
        .with_resolved_expression(|resolved| {
62
21
            specialise_literal_matrix_index(resolved, indices.as_slice())
63
21
        })
64
        .flatten()
65
    {
66
        return Ok(RuleEffect::pure(projected));
67
460507
    }
68

            
69
460507
    let constant = reference.resolve_constant().ok_or(RuleNotApplicable)?;
70
21
    let Lit::AbstractLiteral(AbstractLiteral::Matrix(_, _)) = &constant else {
71
        return Err(RuleNotApplicable);
72
    };
73

            
74
21
    Ok(RuleEffect::pure(Expr::SafeIndex(
75
21
        Metadata::new(),
76
21
        Moo::new(Expr::Atomic(Metadata::new(), Atom::Literal(constant))),
77
21
        indices.clone(),
78
21
    )))
79
509684
}
80

            
81
14
fn literal_matrix_offset(domain: &GroundDomain, index: i32) -> Option<usize> {
82
14
    let GroundDomain::Int(ranges, _) = domain else {
83
        return None;
84
    };
85
14
    let [range] = ranges.as_slice() else {
86
14
        return None;
87
    };
88
    usize::try_from(index.checked_sub(*range.low()?)?).ok()
89
14
}
90

            
91
14
fn literal_at_matrix_indices<'a>(mut value: &'a Lit, indices: &[i32]) -> Option<&'a Lit> {
92
14
    for &index in indices {
93
14
        let Lit::AbstractLiteral(AbstractLiteral::Matrix(elems, domain)) = value else {
94
            return None;
95
        };
96
14
        value = elems.get(literal_matrix_offset(domain, index)?)?;
97
    }
98
    Some(value)
99
14
}
100

            
101
/// Projects fixed trailing dimensions from a directly stored literal matrix.
102
///
103
/// For example, `m[i, 2]` becomes `[m[1,2], ..., m[n,2]][i]` in one rewrite, without cloning `m`
104
/// or constructing `n` intermediate SafeIndex expressions.
105
21
fn specialise_literal_matrix_index(resolved: &Expr, indices: &[Expr]) -> Option<Expr> {
106
21
    if indices.len() < 2 {
107
        return None;
108
21
    }
109

            
110
    let Expr::Atomic(
111
        _,
112
        Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Matrix(outer_elems, outer_domain))),
113
21
    ) = resolved
114
    else {
115
        return None;
116
    };
117

            
118
21
    let trailing_indices = indices[1..]
119
21
        .iter()
120
21
        .map(|index| match eval_constant(index)? {
121
14
            Lit::Int(value) => Some(value),
122
            _ => None,
123
21
        })
124
7
        .collect::<Option<Vec<_>>>()?;
125

            
126
14
    let projected = outer_elems
127
14
        .iter()
128
14
        .map(|row| literal_at_matrix_indices(row, &trailing_indices).cloned())
129
14
        .collect::<Option<Vec<_>>>()?;
130

            
131
    Some(Expr::SafeIndex(
132
        Metadata::new(),
133
        Moo::new(Expr::Atomic(
134
            Metadata::new(),
135
            Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Matrix(
136
                projected,
137
                outer_domain.clone(),
138
            ))),
139
        )),
140
        vec![indices[0].clone()],
141
    ))
142
21
}
143

            
144
/// Lower modelling collections to Minion's scalar table relation.
145
#[register_rule("Minion", 4050, [Table, NegativeTable])]
146
1675
fn flatten_table(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
147
1675
    let (inputs, rows, negative) = match expr {
148
686
        Expr::Table(_, inputs, rows) => (inputs, rows, false),
149
42
        Expr::NegativeTable(_, inputs, rows) => (inputs, rows, true),
150
947
        _ => return Err(RuleNotApplicable),
151
    };
152
728
    let inputs = crate::shared::utils::table_ordered_entries(inputs).ok_or(RuleNotApplicable)?;
153
728
    let rows = crate::shared::utils::table_rows(rows).ok_or(RuleNotApplicable)?;
154
728
    let rows = rows
155
728
        .iter()
156
3913
        .map(|row| {
157
3913
            let row = crate::shared::utils::table_ordered_entries(row)?;
158
3913
            if row.len() != inputs.len() {
159
                return None;
160
3913
            }
161
3913
            row.iter()
162
11424
                .map(|value| match eval_constant(value)? {
163
11424
                    Lit::Int(value) => Some(value),
164
                    Lit::Bool(value) => Some(i32::from(value)),
165
                    _ => None,
166
11424
                })
167
                .collect::<Option<Vec<_>>>()
168
3913
        })
169
        .collect::<Option<Vec<_>>>()
170
728
        .ok_or(RuleNotApplicable)?;
171
728
    let mut symbols = symbols.clone();
172
728
    let mut top = vec![];
173
728
    let inputs = inputs
174
728
        .into_iter()
175
2051
        .map(|input| flatten_expression_to_atom(input, &mut symbols, &mut top))
176
        .collect::<Result<Vec<_>, _>>()?;
177
728
    Ok(RuleEffect::new(
178
728
        Expr::FlatTable(Metadata::new(), inputs, rows, negative),
179
728
        top,
180
728
        symbols,
181
728
    ))
182
1675
}
183

            
184
#[register_rule("Minion", 4200, [Eq / Product, AuxDeclaration / Product])]
185
2152
fn introduce_producteq(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
186
    // product = val
187
    let val: Atom;
188
    let product: Moo<Expr>;
189

            
190
2152
    match expr.clone() {
191
        Expr::Eq(_m, a, b) => {
192
253
            let a_atom: Option<&Atom> = (&a).try_into().ok();
193
253
            let b_atom: Option<&Atom> = (&b).try_into().ok();
194

            
195
253
            if let Some(f) = a_atom {
196
                // val = product
197
79
                val = f.clone();
198
79
                product = b;
199
174
            } else if let Some(f) = b_atom {
200
                // product = val
201
84
                val = f.clone();
202
84
                product = a;
203
            } else {
204
90
                return Err(RuleNotApplicable);
205
            }
206
        }
207
        Expr::AuxDeclaration(_m, reference, e) => {
208
346
            val = Atom::Reference(reference);
209
346
            product = e;
210
        }
211
        _ => {
212
1553
            return Err(RuleNotApplicable);
213
        }
214
    }
215

            
216
509
    if !(matches!(&*product, Expr::Product(_, _,))) {
217
48
        return Err(RuleNotApplicable);
218
461
    }
219

            
220
461
    let Expr::Product(_, factors) = &*product else {
221
        return Err(RuleNotApplicable);
222
    };
223

            
224
461
    let mut factors_vec = (**factors).clone().into_list().ok_or(RuleNotApplicable)?;
225
461
    if factors_vec.len() < 2 {
226
        return Err(RuleNotApplicable);
227
461
    }
228

            
229
    // Product is a vecop, but FlatProductEq a binop.
230
    // Introduce auxvars until it is a binop
231

            
232
    // the expression returned will be x*y=val.
233
    // if factors is > 2 arguments, y will be an auxiliary variable
234

            
235
    #[allow(clippy::unwrap_used)] // should never panic - length is checked above
236
461
    let x: Atom = factors_vec
237
461
        .pop()
238
461
        .unwrap()
239
461
        .try_into()
240
461
        .or(Err(RuleNotApplicable))?;
241

            
242
    #[allow(clippy::unwrap_used)] // should never panic - length is checked above
243
423
    let mut y: Atom = factors_vec
244
423
        .pop()
245
423
        .unwrap()
246
423
        .try_into()
247
423
        .or(Err(RuleNotApplicable))?;
248

            
249
423
    let mut symbols = symbols.clone();
250
423
    let mut new_tops: Vec<Expr> = vec![];
251

            
252
    // FIXME: add a test for this
253
640
    while let Some(next_factor) = factors_vec.pop() {
254
        // Despite adding auxvars, I still require all atoms as factors, making this rule act
255
        // similar to other introduction rules.
256
217
        let next_factor_atom: Atom = next_factor.clone().try_into().or(Err(RuleNotApplicable))?;
257

            
258
        // TODO: find this domain without having to make unnecessary Expr and Metadata objects
259
        // Just using the domain of expr doesn't work
260
217
        let aux_domain = Expr::Product(
261
217
            Metadata::new(),
262
217
            Moo::new(matrix_expr![y.clone().into(), next_factor]),
263
217
        )
264
217
        .domain_of()
265
        // Essence domain_of rejects bool arithmetic; Minion treats BOOL as 0/1.
266
        .or_else(|| minion_product_aux_domain_from_atoms(&y, &next_factor_atom))
267
217
        .ok_or(ApplicationError::DomainError)?;
268

            
269
217
        let aux_decl = symbols.gen_find_auxiliary(&aux_domain);
270
217
        let aux_var = Atom::Reference(Reference::new(aux_decl));
271

            
272
217
        let new_top_expr = Expr::FlatProductEq(
273
217
            Metadata::new(),
274
217
            Moo::new(y),
275
217
            Moo::new(next_factor_atom),
276
217
            Moo::new(aux_var.clone()),
277
217
        );
278

            
279
217
        new_tops.push(new_top_expr);
280
217
        y = aux_var;
281
    }
282

            
283
423
    Ok(RuleEffect::new(
284
423
        Expr::FlatProductEq(Metadata::new(), Moo::new(x), Moo::new(y), Moo::new(val)),
285
423
        new_tops,
286
423
        symbols,
287
423
    ))
288
2152
}
289

            
290
/// Introduces `FlatWeightedSumLeq`, `FlatWeightedSumGeq`, `FlatSumLeq`, FlatSumGeq` constraints.
291
///
292
/// If the input is a weighted sum, the weighted sum constraints are used, otherwise the standard
293
/// sum constraints are used.
294
///
295
/// # Details
296
/// This rule is a bit unusual compared to other introduction rules in that
297
/// it does its own flattening.
298
///
299
/// Weighted sums are expressed as sums of products, which are not
300
/// flat. Flattening a weighted sum generically makes it indistinguishable
301
/// from a sum:
302
///
303
///```text
304
/// 1*a + 2*b + 3*c + d <= 10
305
///   ~~> flatten_vecop
306
/// __0 + __1 + __2 + d <= 10
307
///
308
///
309
/// __0 =aux 1*a
310
/// __1 =aux 2*b
311
/// __2 =aux 3*c
312
/// ```
313
///
314
/// Therefore, introduce_weightedsumleq_sumgeq does its own flattening.
315
///
316
/// Having custom flattening semantics means that we can make more things
317
/// weighted sums.
318
///
319
/// For example, consider `a + 2*b + 3*c*d + (e / f) + 5*(g/h) <= 18`. This
320
/// rule turns this into a single flat_weightedsumleq constraint:
321
///
322
///```text
323
/// a + 2*b + 3*c*d + (e/f) + 5*(g/h) <= 30
324
///
325
///   ~> introduce_weightedsumleq_sumgeq
326
///
327
/// flat_weightedsumleq([1,2,3,1,5],[a,b,__0,__1,__2],30)
328
///
329
/// with new top level constraints
330
///
331
/// __0 = c*d
332
/// __1 = e/f
333
/// __2 = g/h
334
/// ```
335
///
336
/// The rules to turn terms into coefficient variable pairs are the following:
337
///
338
/// 1. Non-weighted atom: `a ~> (1,a)`
339
/// 2. Other non-weighted term: `e ~> (1,__0)`, with new constraint `__0 =aux e`
340
/// 3. Weighted atom: `c*a ~> (c,a)`
341
/// 4. Weighted non-atom: `c*e ~> (c,__0)` with new constraint` __0 =aux e`
342
/// 5. Weighted product: `c*e*f ~> (c,__0)` with new constraint `__0 =aux (e*f)`
343
/// 6. Negated atom: `-x ~> (-1,x)`
344
/// 7. Negated expression: `-e ~> (-1,__0)` with new constraint `__0 = e`
345
///
346
/// Cases 6 and 7 could potentially be a normalising rule `-e ~> -1*e`. However, I think that we
347
/// should only turn negations into a product when they are inside a sum, not all the time.
348
#[register_rule("Minion", 4600, [Leq / Sum, Geq / Sum, Eq / Sum, AuxDeclaration / Sum])]
349
25591
fn introduce_weighted_sumleq_sumgeq(expr: &Expr, symtab: &SymbolTable) -> ApplicationResult {
350
    // Keep track of which type of (in)equality was in the input, and use this to decide what
351
    // constraints to make at the end
352

            
353
    // We handle Eq directly in this rule instead of letting it be decomposed to <= and >=
354
    // elsewhere, as this caused cyclic rule application:
355
    //
356
    // ```
357
    // 2*a + b = c
358
    //
359
    //   ~~> sumeq_to_inequalities
360
    //
361
    // 2*a + b <=c /\ 2*a + b >= c
362
    //
363
    // --
364
    //
365
    // 2*a + b <= c
366
    //
367
    //   ~~> flatten_generic
368
    // __1 <=c
369
    //
370
    // with new top level constraint
371
    //
372
    // 2*a + b =aux __1
373
    //
374
    // --
375
    //
376
    // 2*a + b =aux __1
377
    //
378
    //   ~~> sumeq_to_inequalities
379
    //
380
    // LOOP!
381
    // ```
382
    enum EqualityKind {
383
        Eq,
384
        Leq,
385
        Geq,
386
    }
387

            
388
    // Given the LHS, RHS, and the type of inequality, return the sum, total, and new inequality.
389
    //
390
    // The inequality returned is the one that puts the sum is on the left hand side and the total
391
    // on the right hand side.
392
    //
393
    // For example, `1 <= a + b` will result in ([a,b],1,Geq).
394
13368
    fn match_sum_total(
395
13368
        a: Moo<Expr>,
396
13368
        b: Moo<Expr>,
397
13368
        equality_kind: EqualityKind,
398
13368
    ) -> Result<(Vec<Expr>, Atom, EqualityKind), ApplicationError> {
399
13368
        match (
400
13368
            Moo::unwrap_or_clone(a),
401
13368
            Moo::unwrap_or_clone(b),
402
13368
            equality_kind,
403
13368
        ) {
404
            (Expr::Sum(_, sum_terms), Expr::Atomic(_, total), EqualityKind::Leq) => {
405
1699
                let sum_terms = Moo::unwrap_or_clone(sum_terms)
406
1699
                    .into_list()
407
1699
                    .ok_or(RuleNotApplicable)?;
408
1349
                Ok((sum_terms, total, EqualityKind::Leq))
409
            }
410
            (Expr::Atomic(_, total), Expr::Sum(_, sum_terms), EqualityKind::Leq) => {
411
3329
                let sum_terms = Moo::unwrap_or_clone(sum_terms)
412
3329
                    .into_list()
413
3329
                    .ok_or(RuleNotApplicable)?;
414
3325
                Ok((sum_terms, total, EqualityKind::Geq))
415
            }
416
            (Expr::Sum(_, sum_terms), Expr::Atomic(_, total), EqualityKind::Geq) => {
417
1289
                let sum_terms = Moo::unwrap_or_clone(sum_terms)
418
1289
                    .into_list()
419
1289
                    .ok_or(RuleNotApplicable)?;
420
1142
                Ok((sum_terms, total, EqualityKind::Geq))
421
            }
422
            (Expr::Atomic(_, total), Expr::Sum(_, sum_terms), EqualityKind::Geq) => {
423
756
                let sum_terms = Moo::unwrap_or_clone(sum_terms)
424
756
                    .into_list()
425
756
                    .ok_or(RuleNotApplicable)?;
426
728
                Ok((sum_terms, total, EqualityKind::Leq))
427
            }
428
            (Expr::Sum(_, sum_terms), Expr::Atomic(_, total), EqualityKind::Eq) => {
429
5005
                let sum_terms = Moo::unwrap_or_clone(sum_terms)
430
5005
                    .into_list()
431
5005
                    .ok_or(RuleNotApplicable)?;
432
4529
                Ok((sum_terms, total, EqualityKind::Eq))
433
            }
434
            (Expr::Atomic(_, total), Expr::Sum(_, sum_terms), EqualityKind::Eq) => {
435
1006
                let sum_terms = Moo::unwrap_or_clone(sum_terms)
436
1006
                    .into_list()
437
1006
                    .ok_or(RuleNotApplicable)?;
438
844
                Ok((sum_terms, total, EqualityKind::Eq))
439
            }
440
284
            _ => Err(RuleNotApplicable),
441
        }
442
13368
    }
443

            
444
25591
    let (sum_exprs, total, equality_kind) = match expr.clone() {
445
5173
        Expr::Leq(_, a, b) => Ok(match_sum_total(a, b, EqualityKind::Leq)?),
446
2045
        Expr::Geq(_, a, b) => Ok(match_sum_total(a, b, EqualityKind::Geq)?),
447
6150
        Expr::Eq(_, a, b) => Ok(match_sum_total(a, b, EqualityKind::Eq)?),
448
        Expr::AuxDeclaration(_, reference, a) => {
449
9282
            let total: Atom = Atom::Reference(reference);
450
9282
            if let Expr::Sum(_, sum_terms) = Moo::unwrap_or_clone(a) {
451
9192
                let sum_terms = Moo::unwrap_or_clone(sum_terms)
452
9192
                    .into_list()
453
9192
                    .ok_or(RuleNotApplicable)?;
454
8775
                Ok((sum_terms, total, EqualityKind::Eq))
455
            } else {
456
90
                Err(RuleNotApplicable)
457
            }
458
        }
459
2941
        _ => Err(RuleNotApplicable),
460
3031
    }?;
461

            
462
20692
    let mut new_top_exprs: Vec<Expr> = vec![];
463
20692
    let mut symtab = symtab.clone();
464

            
465
    #[allow(clippy::mutable_key_type)]
466
20692
    let mut coefficients_and_vars: HashMap<Atom, i32> = HashMap::new();
467

            
468
    // Every summand must stand for a single term. An unexpanded comprehension stands for many, and
469
    // flattening it here mints an auxiliary holding the whole matrix, which no solver can take.
470
    // Leave it for comprehension expansion. (A comprehension's return type is its element type, so
471
    // this has to match the variant rather than ask for the type.)
472
20692
    if sum_exprs
473
20692
        .iter()
474
62354
        .any(|term| matches!(term, Expr::Comprehension(_, _)))
475
    {
476
        return Err(RuleNotApplicable);
477
20692
    }
478

            
479
    // for each sub-term, get the coefficient and the variable, flattening if necessary.
480
    //
481
20692
    for expr in sum_exprs {
482
61906
        let (coeff, var) = flatten_weighted_sum_term(expr, &mut symtab, &mut new_top_exprs)?;
483

            
484
61458
        if coeff == 0 {
485
            continue;
486
61458
        }
487

            
488
        // collect coefficients for like terms, so 2*x + -1*x ~~> 1*x
489
61458
        coefficients_and_vars
490
61458
            .entry(var)
491
98
            .and_modify(|x| *x += coeff)
492
61458
            .or_insert(coeff);
493
    }
494

            
495
    // the expr should use a regular sum instead if the coefficients are all 1.
496
57661
    let use_weighted_sum = !coefficients_and_vars.values().all(|x| *x == 1 || *x == 0);
497

            
498
    // This needs a consistent iteration order so that the output is deterministic. However,
499
    // HashMap doesn't provide this. Can't use BTreeMap or Ord to achieve this, as not everything
500
    // in the AST implements Ord. Instead, order things by their pretty printed value.
501
20244
    let (vars, coefficients): (Vec<Atom>, Vec<Lit>) = coefficients_and_vars
502
20244
        .into_iter()
503
61360
        .filter(|(_, c)| *c != 0)
504
99810
        .sorted_by(|a, b| {
505
99810
            let a_atom_str = format!("{}", a.0);
506
99810
            let b_atom_str = format!("{}", b.0);
507
99810
            a_atom_str.cmp(&b_atom_str)
508
99810
        })
509
61353
        .map(|(v, c)| (v, Lit::Int(c)))
510
        .unzip();
511

            
512
20244
    let new_expr: Expr = match (equality_kind, use_weighted_sum) {
513
3016
        (EqualityKind::Eq, true) => Expr::And(
514
3016
            Metadata::new(),
515
3016
            Moo::new(matrix_expr![
516
3016
                Expr::FlatWeightedSumLeq(
517
3016
                    Metadata::new(),
518
3016
                    coefficients.clone(),
519
3016
                    vars.clone(),
520
3016
                    Moo::new(total.clone()),
521
3016
                ),
522
3016
                Expr::FlatWeightedSumGeq(Metadata::new(), coefficients, vars, Moo::new(total)),
523
3016
            ]),
524
3016
        ),
525
10684
        (EqualityKind::Eq, false) => Expr::And(
526
10684
            Metadata::new(),
527
10684
            Moo::new(matrix_expr![
528
10684
                Expr::FlatSumLeq(Metadata::new(), vars.clone(), total.clone()),
529
10684
                Expr::FlatSumGeq(Metadata::new(), vars, total),
530
10684
            ]),
531
10684
        ),
532
        (EqualityKind::Leq, true) => {
533
161
            Expr::FlatWeightedSumLeq(Metadata::new(), coefficients, vars, Moo::new(total))
534
        }
535
1916
        (EqualityKind::Leq, false) => Expr::FlatSumLeq(Metadata::new(), vars, total),
536
        (EqualityKind::Geq, true) => {
537
28
            Expr::FlatWeightedSumGeq(Metadata::new(), coefficients, vars, Moo::new(total))
538
        }
539
4439
        (EqualityKind::Geq, false) => Expr::FlatSumGeq(Metadata::new(), vars, total),
540
    };
541

            
542
20244
    Ok(RuleEffect::new(new_expr, new_top_exprs, symtab))
543
25591
}
544

            
545
/// For a term inside a weighted sum, return coefficient*variable.
546
///
547
///
548
/// If the term is in the form <coefficient> * <non flat expression>, the expression is flattened
549
/// to a new auxvar, which is returned as the variable for this term.
550
///
551
/// New auxvars are added to `symtab`, and their top level constraints to `top_level_exprs`.
552
///
553
/// # Errors
554
///
555
/// + Returns [`ApplicationError::RuleNotApplicable`] if a non-flat expression cannot be turned
556
///   into an atom. See [`flatten_expression_to_atom`].
557
///
558
/// + Returns [`ApplicationError::RuleNotApplicable`] if the term is a product containing a matrix
559
///   literal, and that matrix literal is not a list.
560
///
561
///
562
2494
fn try_eval_i32_coeff(expr: &Expr) -> Option<i32> {
563
2494
    if let Expr::Atomic(_, Atom::Literal(Lit::Int(coeff))) = expr {
564
        return Some(*coeff);
565
2494
    }
566
2494
    eval_constant(expr).and_then(|lit| match lit {
567
        Lit::Int(coeff) => Some(coeff),
568
        _ => None,
569
    })
570
2494
}
571

            
572
2494
fn flatten_weighted_sum_product_factor(
573
2494
    expr: Expr,
574
2494
    symtab: &mut SymbolTable,
575
2494
    top_level_exprs: &mut Vec<Expr>,
576
2494
) -> Result<Atom, ApplicationError> {
577
2494
    if let Expr::Atomic(_, atom) = &expr {
578
168
        return Ok(atom.clone());
579
2326
    }
580

            
581
    // Parameter or constant matrix lookups can appear as coefficients inside sums.
582
2326
    if let Expr::SafeIndex(_, subject, indices) = &expr {
583
35
        let indices_are_atomic = indices
584
35
            .iter()
585
35
            .all(|index| matches!(index, Expr::Atomic(_, _)));
586
35
        if indices_are_atomic
587
35
            && let Expr::Atomic(_, Atom::Reference(reference)) = subject.as_ref()
588
            && matches!(
589
                reference.category_of(),
590
                Category::Parameter | Category::Constant
591
            )
592
        {
593
            let domain = expr.domain_of().ok_or(RuleNotApplicable)?;
594
            let decl = symtab.gen_find_auxiliary(&domain);
595
            top_level_exprs.push(Expr::AuxDeclaration(
596
                Metadata::new(),
597
                Reference::new(decl.clone()),
598
                Moo::new(expr),
599
            ));
600
            return Ok(Atom::Reference(Reference::new(decl)));
601
35
        }
602
2291
    }
603

            
604
2326
    flatten_expression_to_atom(expr, symtab, top_level_exprs)
605
2494
}
606

            
607
1247
fn flatten_weighted_sum_binary_product(
608
1247
    a: Expr,
609
1247
    b: Expr,
610
1247
    symtab: &mut SymbolTable,
611
1247
    top_level_exprs: &mut Vec<Expr>,
612
1247
) -> Result<Atom, ApplicationError> {
613
1247
    let a_atom = flatten_weighted_sum_product_factor(a, symtab, top_level_exprs)?;
614
1247
    let b_atom = flatten_weighted_sum_product_factor(b, symtab, top_level_exprs)?;
615

            
616
1247
    let product = Expr::Product(
617
1247
        Metadata::new(),
618
1247
        Moo::new(matrix_expr![
619
1247
            Expr::Atomic(Metadata::new(), a_atom.clone()),
620
1247
            Expr::Atomic(Metadata::new(), b_atom.clone()),
621
1247
        ]),
622
1247
    );
623
    // Essence domain_of rejects bool arithmetic; after stripping toInt(bool) for
624
    // Minion we still need an int aux domain (BOOL is 0/1 in the solver).
625
1247
    let domain = product
626
1247
        .domain_of()
627
1099
        .or_else(|| minion_product_aux_domain_from_atoms(&a_atom, &b_atom))
628
1247
        .ok_or(RuleNotApplicable)?;
629
1247
    let decl = symtab.gen_find_auxiliary(&domain);
630
1247
    let aux = Atom::Reference(Reference::new(decl));
631

            
632
1247
    top_level_exprs.push(Expr::FlatProductEq(
633
1247
        Metadata::new(),
634
1247
        Moo::new(a_atom),
635
1247
        Moo::new(b_atom),
636
1247
        Moo::new(aux.clone()),
637
1247
    ));
638

            
639
1247
    Ok(aux)
640
1247
}
641

            
642
/// Minion treats BOOL as 0/1. Use that only when emitting solver constraints —
643
/// not for Essence typechecking / `domain_of`.
644
2279
fn minion_atom_as_i32_bounds(atom: &Atom) -> Option<(i32, i32)> {
645
2279
    let resolved = atom.domain_of().resolve().ok()?;
646
2279
    match resolved.as_ref() {
647
2171
        GroundDomain::Bool => Some((0, 1)),
648
        GroundDomain::Int(ranges, _) => {
649
108
            let mut lo = i32::MAX;
650
108
            let mut hi = i32::MIN;
651
108
            for range in ranges {
652
108
                match range {
653
                    Range::Single(x) => {
654
39
                        lo = lo.min(*x);
655
39
                        hi = hi.max(*x);
656
                    }
657
                    Range::Bounded(a, b) => {
658
69
                        lo = lo.min(*a);
659
69
                        hi = hi.max(*b);
660
                    }
661
                    _ => return None,
662
                }
663
            }
664
108
            (lo <= hi).then_some((lo, hi))
665
        }
666
        _ => None,
667
    }
668
2279
}
669

            
670
1099
fn minion_product_aux_domain_from_atoms(a: &Atom, b: &Atom) -> Option<DomainPtr> {
671
1099
    let (a_lo, a_hi) = minion_atom_as_i32_bounds(a)?;
672
1099
    let (b_lo, b_hi) = minion_atom_as_i32_bounds(b)?;
673
    let corners = [
674
1099
        a_lo.checked_mul(b_lo)?,
675
1099
        a_lo.checked_mul(b_hi)?,
676
1099
        a_hi.checked_mul(b_lo)?,
677
1099
        a_hi.checked_mul(b_hi)?,
678
    ];
679
1099
    let lo = *corners.iter().min()?;
680
1099
    let hi = *corners.iter().max()?;
681
1099
    if lo == hi {
682
        Some(Domain::int(vec![Range::Single(lo)]))
683
    } else {
684
1099
        Some(Domain::int(vec![Range::Bounded(lo, hi)]))
685
    }
686
1099
}
687

            
688
61906
fn flatten_weighted_sum_term(
689
61906
    term: Expr,
690
61906
    symtab: &mut SymbolTable,
691
61906
    top_level_exprs: &mut Vec<Expr>,
692
61906
) -> Result<(i32, Atom), ApplicationError> {
693
61906
    match term {
694
        // we can only see check the product for coefficients it contains a matrix literal.
695
        //
696
        // e.g. the input expression `product([2,x])` returns (2,x), but `product(my_matrix)`
697
        // returns (1,product(my_matrix)).
698
        //
699
        // if the product contains a matrix literal but it is not a list, throw `RuleNotApplicable`
700
        // to allow it to be changed into a list by another rule.
701
4704
        Expr::Product(_, factors) if factors.is_matrix_literal() => {
702
            // this fails if factors is not a matrix literal or that matrix literal is not a list.
703
            //
704
            // we already check for the first case above, so this should only error when we have a
705
            // non-list matrix literal.
706
4704
            let factors = Moo::unwrap_or_clone(factors)
707
4704
                .into_list()
708
4704
                .ok_or(RuleNotApplicable)?;
709

            
710
4704
            match factors.as_slice() {
711
                // product([]) ~~> (0,0)
712
                // coefficients of 0 should be ignored by the caller.
713
                [] => Ok((0, Atom::Literal(Lit::Int(0)))),
714

            
715
                // assume the coefficients have been placed at the front by normalisation rules
716

            
717
                // product[1,x,y,...] ~> return (coeff,product([x,y,...]))
718
                [
719
                    Expr::Atomic(_, Atom::Literal(Lit::Int(coeff))),
720
                    e,
721
                    rest @ ..,
722
                ] => {
723
                    // A lone remaining factor is the term itself. Wrapping it back up as
724
                    // `product([x])` would ask for an auxiliary holding a one-element product,
725
                    // which has no integer domain when `x` is a bool -- and a bool is exactly what
726
                    // `flatten_product` leaves behind, since Minion reads it as 0/1.
727
3415
                    if rest.is_empty() {
728
                        return Ok((
729
3342
                            *coeff,
730
3342
                            flatten_expression_to_atom(e.clone(), symtab, top_level_exprs)?,
731
                        ));
732
73
                    }
733

            
734
73
                    let mut product_terms = Vec::from(rest);
735
73
                    product_terms.push(e.clone());
736
                    let product =
737
73
                        Expr::Product(Metadata::new(), Moo::new(into_matrix_expr!(product_terms)));
738
                    Ok((
739
73
                        *coeff,
740
73
                        flatten_expression_to_atom(product, symtab, top_level_exprs)?,
741
                    ))
742
                }
743

            
744
                // product([c,y]) or product([y,c]) when one factor is a constant coefficient
745
                [a, b] => {
746
1247
                    if let Some(coeff) = try_eval_i32_coeff(a) {
747
                        Ok((
748
                            coeff,
749
                            flatten_expression_to_atom(b.clone(), symtab, top_level_exprs)?,
750
                        ))
751
1247
                    } else if let Some(coeff) = try_eval_i32_coeff(b) {
752
                        Ok((
753
                            coeff,
754
                            flatten_expression_to_atom(a.clone(), symtab, top_level_exprs)?,
755
                        ))
756
                    } else {
757
                        Ok((
758
1247
                            1,
759
1247
                            flatten_weighted_sum_binary_product(
760
1247
                                a.clone(),
761
1247
                                b.clone(),
762
1247
                                symtab,
763
1247
                                top_level_exprs,
764
1247
                            )?,
765
                        ))
766
                    }
767
                }
768

            
769
                // product([x,y,z,...]) ~~> (1,product([x,y,z,...]))
770
                _ => {
771
                    let product =
772
42
                        Expr::Product(Metadata::new(), Moo::new(into_matrix_expr!(factors)));
773
                    Ok((
774
42
                        1,
775
42
                        flatten_expression_to_atom(product, symtab, top_level_exprs)?,
776
                    ))
777
                }
778
            }
779
        }
780
        Expr::Neg(_, inner_term) => Ok((
781
833
            -1,
782
833
            flatten_expression_to_atom(Moo::unwrap_or_clone(inner_term), symtab, top_level_exprs)?,
783
        )),
784
        term => Ok((
785
56369
            1,
786
56369
            flatten_expression_to_atom(term, symtab, top_level_exprs)?,
787
        )),
788
    }
789
61906
}
790

            
791
/// Converts the input expression to an atom, placing it into a new auxiliary variable if
792
/// necessary.
793
///
794
/// The auxiliary variable will be added to the symbol table and its top-level-constraint to
795
/// `top_level_exprs`.
796
///
797
/// If the expression is already atomic, no auxiliary variables are created, and the atom is
798
/// returned as-is.
799
///
800
/// # Errors
801
///
802
///  + Returns [`ApplicationError::RuleNotApplicable`] if the expression cannot be placed into an
803
///    auxiliary variable. For example, expressions that do not have domains.
804
///
805
///    This function supports the same expressions as [`to_aux_var_in`], except that this function
806
///    succeeds when the expression given is atomic.
807
///
808
///    See [`to_aux_var_in`] for more information.
809
///
810
65760
fn flatten_expression_to_atom(
811
65760
    expr: Expr,
812
65760
    symtab: &mut SymbolTable,
813
65760
    top_level_exprs: &mut Vec<Expr>,
814
65760
) -> Result<Atom, ApplicationError> {
815
65760
    if let Expr::Atomic(_, atom) = expr {
816
31796
        return Ok(atom);
817
33964
    }
818

            
819
    // Minion treats BOOL as 0/1, so `toInt(atom)` needs no aux variable.
820
    // SavileRow posts bools directly into sumleq/sumgeq; without this, every
821
    // `toInt(b)` becomes `__ =aux toInt(b)` and roughly doubles the variable count
822
    // on models like solitaire_battleship.
823
33964
    if let Expr::ToInt(_, inner) = &expr
824
20938
        && let Expr::Atomic(_, atom) = inner.as_ref()
825
    {
826
17028
        return Ok(atom.clone());
827
16936
    }
828

            
829
16936
    let (reference, top) = to_aux_var_in(&expr, symtab).ok_or(RuleNotApplicable)?;
830
16488
    top_level_exprs.push(top);
831

            
832
16488
    Ok(Atom::Reference(reference))
833
65760
}
834

            
835
#[register_rule("Minion", 4200, [Eq / SafeDiv, AuxDeclaration / SafeDiv])]
836
31127
fn introduce_diveq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
837
    // div = val
838
    let val: Atom;
839
    let div: Moo<Expr>;
840
    let meta: Metadata;
841

            
842
31127
    match expr.clone() {
843
        Expr::Eq(m, a, b) => {
844
267
            meta = m;
845

            
846
267
            let a_atom: Option<&Atom> = (&a).try_into().ok();
847
267
            let b_atom: Option<&Atom> = (&b).try_into().ok();
848

            
849
267
            if let Some(f) = a_atom {
850
                // val = div
851
128
                val = f.clone();
852
128
                div = b;
853
139
            } else if let Some(f) = b_atom {
854
                // div = val
855
133
                val = f.clone();
856
133
                div = a;
857
            } else {
858
6
                return Err(RuleNotApplicable);
859
            }
860
        }
861
        Expr::AuxDeclaration(m, reference, e) => {
862
29307
            meta = m;
863
29307
            val = Atom::Reference(reference);
864
29307
            div = e;
865
        }
866
        _ => {
867
1553
            return Err(RuleNotApplicable);
868
        }
869
    }
870

            
871
29568
    if !(matches!(&*div, Expr::SafeDiv(_, _, _))) {
872
48
        return Err(RuleNotApplicable);
873
29520
    }
874

            
875
29520
    let Expr::SafeDiv(_, a, b) = div.as_ref() else {
876
        return Err(RuleNotApplicable);
877
    };
878
29520
    let a: &Atom = a.as_ref().try_into().or(Err(RuleNotApplicable))?;
879
29492
    let b: &Atom = b.as_ref().try_into().or(Err(RuleNotApplicable))?;
880

            
881
29478
    Ok(RuleEffect::pure(Expr::MinionDivEqUndefZero(
882
29478
        meta,
883
29478
        Moo::new(a.clone()),
884
29478
        Moo::new(b.clone()),
885
29478
        Moo::new(val),
886
29478
    )))
887
31127
}
888

            
889
#[register_rule("Minion", 4200, [Eq / SafeMod, AuxDeclaration / SafeMod])]
890
59268
fn introduce_modeq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
891
    // div = val
892
    let val: Atom;
893
    let div: Moo<Expr>;
894
    let meta: Metadata;
895

            
896
59268
    match expr.clone() {
897
        Expr::Eq(m, a, b) => {
898
25923
            meta = m;
899
25923
            let a_atom: Option<&Atom> = (&a).try_into().ok();
900
25923
            let b_atom: Option<&Atom> = (&b).try_into().ok();
901

            
902
25923
            if let Some(f) = a_atom {
903
                // val = div
904
156
                val = f.clone();
905
156
                div = b;
906
25767
            } else if let Some(f) = b_atom {
907
                // div = val
908
25705
                val = f.clone();
909
25705
                div = a;
910
            } else {
911
62
                return Err(RuleNotApplicable);
912
            }
913
        }
914
        Expr::AuxDeclaration(m, reference, e) => {
915
31792
            meta = m;
916
31792
            val = Atom::Reference(reference);
917
31792
            div = e;
918
        }
919
        _ => {
920
1553
            return Err(RuleNotApplicable);
921
        }
922
    }
923

            
924
57653
    if !(matches!(&*div, Expr::SafeMod(_, _, _))) {
925
48
        return Err(RuleNotApplicable);
926
57605
    }
927

            
928
57605
    let Expr::SafeMod(_, a, b) = div.as_ref() else {
929
        return Err(RuleNotApplicable);
930
    };
931
57605
    let a: &Atom = a.as_ref().try_into().or(Err(RuleNotApplicable))?;
932
29982
    let b: &Atom = b.as_ref().try_into().or(Err(RuleNotApplicable))?;
933

            
934
29982
    Ok(RuleEffect::pure(Expr::MinionModuloEqUndefZero(
935
29982
        meta,
936
29982
        Moo::new(a.clone()),
937
29982
        Moo::new(b.clone()),
938
29982
        Moo::new(val),
939
29982
    )))
940
59268
}
941

            
942
/// Lowers a Lee-metric atom `min([|e|, K - |e|])` to flat Minion constraints in one step.
943
///
944
/// The alphabet-size form `min([|x - y|, K - |x - y|])` otherwise pays a long micro-pipeline
945
/// (`minus_to_sum`, flatten, `introduce_abseq`, `introduce_mineq`, `matrix_to_list`) and, worse,
946
/// duplicates work on the two copies of `|e|`. This rule runs above `minus_to_sum` (8400) so the
947
/// shared absolute value is preserved, and emits:
948
///
949
/// - `diff` channelled from `e` (weighted sum when `e` is an atomic minus),
950
/// - `AbsEq(abs, diff)`,
951
/// - `abs + complement = K`,
952
/// - `FlatMinEq([abs, complement], result)`.
953
///
954
/// Also accepts the post-`minus_to_sum` complement shape `sum([K, -(|e|)])`.
955
#[register_rule("Minion", 8600, [Min])]
956
4310
fn introduce_lee_distance_atom(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
957
4310
    let Some((inner, alphabet_size)) = match_lee_distance_min(expr) else {
958
4308
        return Err(RuleNotApplicable);
959
    };
960

            
961
2
    let result_domain = expr.domain_of().ok_or(ApplicationError::DomainError)?;
962
2
    let abs_domain = Expr::Abs(Metadata::new(), Moo::new(inner.clone()))
963
2
        .domain_of()
964
2
        .ok_or(ApplicationError::DomainError)?;
965

            
966
2
    let mut symbols = symbols.clone();
967
2
    let mut new_tops: Vec<Expr> = Vec::new();
968

            
969
2
    let diff_atom = flatten_lee_diff_to_atom(inner, &mut symbols, &mut new_tops)?;
970

            
971
2
    let abs_atom = Atom::new_ref(symbols.gen_find_auxiliary(&abs_domain));
972
2
    new_tops.push(Expr::FlatAbsEq(
973
2
        Metadata::new(),
974
2
        Moo::new(abs_atom.clone()),
975
2
        Moo::new(diff_atom),
976
2
    ));
977

            
978
2
    let complement_domain = Domain::int(vec![Range::Bounded(0, alphabet_size)]);
979
2
    let complement_atom = Atom::new_ref(symbols.gen_find_auxiliary(&complement_domain));
980
2
    let alphabet_atom = Atom::Literal(Lit::Int(alphabet_size));
981
2
    new_tops.push(Expr::FlatSumLeq(
982
2
        Metadata::new(),
983
2
        vec![abs_atom.clone(), complement_atom.clone()],
984
2
        alphabet_atom.clone(),
985
2
    ));
986
2
    new_tops.push(Expr::FlatSumGeq(
987
2
        Metadata::new(),
988
2
        vec![abs_atom.clone(), complement_atom.clone()],
989
2
        alphabet_atom,
990
2
    ));
991

            
992
2
    let result_atom = Atom::new_ref(symbols.gen_find_auxiliary(&result_domain));
993
2
    new_tops.push(Expr::FlatMinEq(
994
2
        Metadata::new(),
995
2
        vec![abs_atom, complement_atom],
996
2
        result_atom.clone(),
997
2
    ));
998

            
999
2
    Ok(RuleEffect::new(
2
        Expr::Atomic(Metadata::new(), result_atom),
2
        new_tops,
2
        symbols,
2
    ))
4310
}
/// Match `min([|e|, K - |e|])` / `min([|e|, sum([K, -(|e|)])])`, returning `(e, K)`.
4310
fn match_lee_distance_min(expr: &Expr) -> Option<(Expr, i32)> {
4310
    let Expr::Min(_, inside) = expr else {
4083
        return None;
    };
227
    let (elems, _) = inside.as_ref().clone().unwrap_matrix_unchecked()?;
227
    if elems.len() != 2 {
14
        return None;
213
    }
213
    match_lee_distance_arms(&elems[0], &elems[1])
211
        .or_else(|| match_lee_distance_arms(&elems[1], &elems[0]))
4310
}
424
fn match_lee_distance_arms(abs_arm: &Expr, complement_arm: &Expr) -> Option<(Expr, i32)> {
424
    let Expr::Abs(_, inner) = abs_arm else {
422
        return None;
    };
2
    let alphabet_size = match_lee_complement(complement_arm, inner.as_ref())?;
2
    if alphabet_size <= 0 {
        return None;
2
    }
2
    Some((Moo::unwrap_or_clone(inner.clone()), alphabet_size))
424
}
2
fn match_lee_complement(complement: &Expr, expected_inner: &Expr) -> Option<i32> {
2
    match complement {
        Expr::Minus(_, lhs, rhs) => {
1
            let alphabet_size = as_int_literal(lhs.as_ref())?;
1
            let Expr::Abs(_, inner) = rhs.as_ref() else {
                return None;
            };
1
            (inner.as_ref() == expected_inner).then_some(alphabet_size)
        }
        Expr::Sum(_, matrix) => {
1
            let matrix = matrix.as_ref().clone();
1
            let terms = if matrix.is_list() {
1
                matrix.into_list()
            } else {
                matrix
                    .unwrap_matrix_unchecked()
                    .map(|(elements, _)| elements)
            }?;
1
            if terms.len() != 2 {
                return None;
1
            }
1
            match_lee_sum_complement(&terms[0], &terms[1], expected_inner)
                .or_else(|| match_lee_sum_complement(&terms[1], &terms[0], expected_inner))
        }
        _ => None,
    }
2
}
1
fn match_lee_sum_complement(a: &Expr, b: &Expr, expected_inner: &Expr) -> Option<i32> {
1
    let alphabet_size = as_int_literal(a)?;
1
    let Expr::Neg(_, negand) = b else {
        return None;
    };
1
    let Expr::Abs(_, inner) = negand.as_ref() else {
        return None;
    };
1
    (inner.as_ref() == expected_inner).then_some(alphabet_size)
1
}
2
fn as_int_literal(expr: &Expr) -> Option<i32> {
2
    match expr {
2
        Expr::Atomic(_, Atom::Literal(Lit::Int(n))) => Some(*n),
        _ => None,
    }
2
}
/// Channel `e` into an atom for the Lee abs argument, preferring a flat weighted sum for `a - b`.
2
fn flatten_lee_diff_to_atom(
2
    inner: Expr,
2
    symbols: &mut SymbolTable,
2
    top_level_exprs: &mut Vec<Expr>,
2
) -> Result<Atom, ApplicationError> {
2
    if let Expr::Minus(_, lhs, rhs) = &inner {
2
        let lhs_atom: Result<&Atom, _> = lhs.as_ref().try_into();
2
        let rhs_atom: Result<&Atom, _> = rhs.as_ref().try_into();
2
        if let (Ok(a), Ok(b)) = (lhs_atom, rhs_atom) {
2
            let domain = inner.domain_of().ok_or(ApplicationError::DomainError)?;
2
            let diff = Atom::new_ref(symbols.gen_find_auxiliary(&domain));
2
            let coefficients = vec![Lit::Int(1), Lit::Int(-1)];
2
            let vars = vec![a.clone(), b.clone()];
2
            top_level_exprs.push(Expr::FlatWeightedSumLeq(
2
                Metadata::new(),
2
                coefficients.clone(),
2
                vars.clone(),
2
                Moo::new(diff.clone()),
2
            ));
2
            top_level_exprs.push(Expr::FlatWeightedSumGeq(
2
                Metadata::new(),
2
                coefficients,
2
                vars,
2
                Moo::new(diff.clone()),
2
            ));
2
            return Ok(diff);
        }
    }
    flatten_expression_to_atom(inner, symbols, top_level_exprs)
2
}
/// Lowers a bare `min([a, b, ...])` to an auxiliary bound by Minion's native `min` constraint.
///
/// Priority is above Base `min_to_var` so Minion keeps Savile Row's `min(vars, aux)` encoding
/// instead of expanding into leq/or/eq constraints that become watched-or.
#[register_rule("Minion", 6100, [Min])]
4217
fn introduce_mineq_from_min(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
4217
    let Expr::Min(_, inside_min_expr) = expr else {
3992
        return Err(RuleNotApplicable);
    };
225
    let Some(exprs) = inside_min_expr.as_ref().clone().into_list() else {
147
        return Err(RuleNotApplicable);
    };
78
    if exprs.is_empty() {
        return Err(RuleNotApplicable);
78
    }
78
    let domain = expr.domain_of().ok_or(ApplicationError::DomainError)?;
78
    let mut symbols = symbols.clone();
78
    let mut new_tops: Vec<Expr> = Vec::new();
78
    let mut atoms = Vec::with_capacity(exprs.len());
78
    for child in exprs {
163
        atoms.push(flatten_expression_to_atom(
163
            child,
163
            &mut symbols,
163
            &mut new_tops,
163
        )?);
    }
78
    let aux = Atom::new_ref(symbols.gen_find_auxiliary(&domain));
78
    new_tops.push(Expr::FlatMinEq(Metadata::new(), atoms, aux.clone()));
78
    Ok(RuleEffect::new(
78
        Expr::Atomic(Metadata::new(), aux),
78
        new_tops,
78
        symbols,
78
    ))
4217
}
/// Lowers `x = min([a, b, ...])` / auxiliary declarations of `min` to [`Expr::FlatMinEq`].
#[register_rule("Minion", 6100, [Eq / Min, AuxDeclaration / Min])]
4070
fn introduce_mineq(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
4070
    let (result, min_expr): (Atom, Expr) = match expr.clone() {
        Expr::Eq(_, a, b) => {
122
            let a = Moo::unwrap_or_clone(a);
122
            let b = Moo::unwrap_or_clone(b);
122
            let a_atom: Option<&Atom> = (&a).try_into().ok();
122
            let b_atom: Option<&Atom> = (&b).try_into().ok();
122
            if let Some(a_atom) = a_atom {
84
                Ok((a_atom.clone(), b))
38
            } else if let Some(b_atom) = b_atom {
14
                Ok((b_atom.clone(), a))
            } else {
24
                Err(RuleNotApplicable)
            }
        }
        Expr::AuxDeclaration(_, reference, inner) => {
173
            Ok((Atom::Reference(reference), Moo::unwrap_or_clone(inner)))
        }
3775
        _ => Err(RuleNotApplicable),
3799
    }?;
271
    let Expr::Min(_, inside_min_expr) = min_expr else {
193
        return Err(RuleNotApplicable);
    };
78
    let Some(exprs) = inside_min_expr.as_ref().clone().into_list() else {
77
        return Err(RuleNotApplicable);
    };
1
    if exprs.is_empty() {
        return Err(RuleNotApplicable);
1
    }
1
    let mut symbols = symbols.clone();
1
    let mut new_tops: Vec<Expr> = Vec::new();
1
    let mut atoms = Vec::with_capacity(exprs.len());
1
    for child in exprs {
2
        atoms.push(flatten_expression_to_atom(
2
            child,
2
            &mut symbols,
2
            &mut new_tops,
2
        )?);
    }
1
    let flat = Expr::FlatMinEq(Metadata::new(), atoms, result);
1
    if new_tops.is_empty() {
1
        Ok(RuleEffect::pure(flat))
    } else {
        Ok(RuleEffect::new(flat, new_tops, symbols))
    }
4070
}
#[register_rule("Minion", 4400, [Eq / Abs, AuxDeclaration / Abs])]
2933
fn introduce_abseq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
2933
    let (x, abs_y): (Atom, Expr) = match expr.clone() {
        Expr::Eq(_, a, b) => {
101
            let a: Expr = Moo::unwrap_or_clone(a);
101
            let b: Expr = Moo::unwrap_or_clone(b);
101
            let a_atom: Option<&Atom> = (&a).try_into().ok();
101
            let b_atom: Option<&Atom> = (&b).try_into().ok();
101
            if let Some(a_atom) = a_atom {
88
                Ok((a_atom.clone(), b))
13
            } else if let Some(b_atom) = b_atom {
7
                Ok((b_atom.clone(), a))
            } else {
6
                Err(RuleNotApplicable)
            }
        }
        Expr::AuxDeclaration(_, reference, expr) => {
175
            let a = Atom::Reference(reference);
175
            let expr = Moo::unwrap_or_clone(expr);
175
            Ok((a, expr))
        }
2657
        _ => Err(RuleNotApplicable),
2663
    }?;
270
    let Expr::Abs(_, y) = abs_y else {
144
        return Err(RuleNotApplicable);
    };
126
    let y = Moo::unwrap_or_clone(y);
126
    let y: Atom = y.try_into().or(Err(RuleNotApplicable))?;
105
    Ok(RuleEffect::pure(Expr::FlatAbsEq(
105
        Metadata::new(),
105
        Moo::new(x),
105
        Moo::new(y),
105
    )))
2933
}
/// Introduces a `MinionPowEq` constraint from a `SafePow`
#[register_rule("Minion", 4200, [Eq / SafePow, AuxDeclaration / SafePow])]
1684
fn introduce_poweq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
1684
    let (a, b, total) = match expr.clone() {
85
        Expr::Eq(_, e1, e2) => match (Moo::unwrap_or_clone(e1), Moo::unwrap_or_clone(e2)) {
            (Expr::Atomic(_, total), Expr::SafePow(_, a, b)) => Ok((a, b, total)),
42
            (Expr::SafePow(_, a, b), Expr::Atomic(_, total)) => Ok((a, b, total)),
43
            _ => Err(RuleNotApplicable),
        },
46
        Expr::AuxDeclaration(_, total_reference, e) => match Moo::unwrap_or_clone(e) {
            Expr::SafePow(_, a, b) => {
46
                let total_ref_atom = Atom::Reference(total_reference);
46
                Ok((a, b, total_ref_atom))
            }
            _ => Err(RuleNotApplicable),
        },
1553
        _ => Err(RuleNotApplicable),
1596
    }?;
88
    let a: Atom = Moo::unwrap_or_clone(a)
88
        .try_into()
88
        .or(Err(RuleNotApplicable))?;
81
    let b: Atom = Moo::unwrap_or_clone(b)
81
        .try_into()
81
        .or(Err(RuleNotApplicable))?;
    // `SafePow` leaves its value unconstrained where `a ** b` is undefined (negative exponents and
    // `0 ** 0`); definedness is enforced separately. Minion's `pow` instead fails on most negative
    // exponents and gives `0 ** 0 = 1`, so it only defines the result where the power is defined.
95
    let can_be_zero = |atom: &Atom| {
95
        atom.domain_of()
95
            .resolve()
95
            .ok()
95
            .is_none_or(|domain| domain.contains(&Lit::Int(0)).unwrap_or(true))
95
    };
81
    let mut defined = Vec::new();
81
    if can_be_zero(&a) && can_be_zero(&b) {
7
        defined.push(Expr::Or(
7
            Metadata::new(),
7
            Moo::new(matrix_expr![
7
                Expr::Neq(
7
                    Metadata::new(),
7
                    Moo::new(Expr::Atomic(Metadata::new(), a.clone())),
7
                    Moo::new(0.into()),
7
                ),
7
                Expr::Neq(
7
                    Metadata::new(),
7
                    Moo::new(Expr::Atomic(Metadata::new(), b.clone())),
7
                    Moo::new(0.into()),
7
                ),
7
            ]),
7
        ));
74
    }
81
    if minion_atom_as_i32_bounds(&b).is_none_or(|(low, _)| low < 0) {
14
        defined.push(Expr::Geq(
14
            Metadata::new(),
14
            Moo::new(Expr::Atomic(Metadata::new(), b.clone())),
14
            Moo::new(0.into()),
14
        ));
67
    }
81
    let pow = Expr::MinionPow(Metadata::new(), Moo::new(a), Moo::new(b), Moo::new(total));
81
    if defined.is_empty() {
67
        return Ok(RuleEffect::pure(pow));
14
    }
14
    let defined = if defined.len() == 1 {
7
        defined.pop().unwrap()
    } else {
7
        Expr::And(Metadata::new(), Moo::new(into_matrix_expr!(defined)))
    };
14
    Ok(RuleEffect::pure(Expr::Imply(
14
        Metadata::new(),
14
        Moo::new(defined),
14
        Moo::new(pow),
14
    )))
1684
}
/// Introduces a `FlatAlldiff` constraint from an `AllDiff`
#[register_rule("Minion", 4200, [AllDiff])]
3796
fn introduce_flat_alldiff(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
3796
    let Expr::AllDiff(_, es) = expr else {
1607
        return Err(RuleNotApplicable);
    };
2189
    let matrix_expr = match es.as_ref() {
147
        Expr::Flatten(_, None, m) => Moo::unwrap_or_clone(m.clone()),
2042
        _ => Moo::unwrap_or_clone(es.clone()),
    };
2189
    let es = if matrix_expr.is_list() {
1477
        matrix_expr.into_list()
    } else {
712
        matrix_expr
712
            .unwrap_matrix_unchecked()
            .map(|(elements, _)| elements)
    }
2189
    .ok_or(RuleNotApplicable)?;
1854
    let atoms = es
1854
        .into_iter()
12949
        .map(|e| match e {
12914
            Expr::Atomic(_, atom) => Ok(atom),
35
            _ => Err(RuleNotApplicable),
12949
        })
1854
        .process_results(|iter| iter.collect_vec())?;
1819
    Ok(RuleEffect::pure(Expr::FlatAllDiff(Metadata::new(), atoms)))
3796
}
497
fn unwrap_flatten_operand(operand: &Moo<Expr>) -> Option<Moo<Expr>> {
497
    match operand.as_ref() {
84
        Expr::Flatten(_, None, inner) => Some(inner.clone()),
413
        _ => None,
    }
497
}
/// Unwraps `flatten(m)` operands in global cardinality constraints.
#[register_rule("Minion", 4050, [AtMost, AtLeast, Gcc, GccWeak])]
1444
fn unwrap_flatten_in_global_cardinality(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
1444
    match expr {
        Expr::AtMost(meta, vars, counts, values) => {
287
            let Some(vars) = unwrap_flatten_operand(vars) else {
203
                return Err(RuleNotApplicable);
            };
84
            Ok(RuleEffect::pure(Expr::AtMost(
84
                meta.clone(),
84
                vars,
84
                counts.clone(),
84
                values.clone(),
84
            )))
        }
        Expr::AtLeast(meta, vars, counts, values) => {
70
            let Some(vars) = unwrap_flatten_operand(vars) else {
70
                return Err(RuleNotApplicable);
            };
            Ok(RuleEffect::pure(Expr::AtLeast(
                meta.clone(),
                vars,
                counts.clone(),
                values.clone(),
            )))
        }
        Expr::Gcc(meta, vars, values, counts) => {
84
            let Some(vars) = unwrap_flatten_operand(vars) else {
84
                return Err(RuleNotApplicable);
            };
            Ok(RuleEffect::pure(Expr::Gcc(
                meta.clone(),
                vars,
                values.clone(),
                counts.clone(),
            )))
        }
        Expr::GccWeak(meta, vars, values, counts) => {
56
            let Some(vars) = unwrap_flatten_operand(vars) else {
56
                return Err(RuleNotApplicable);
            };
            Ok(RuleEffect::pure(Expr::GccWeak(
                meta.clone(),
                vars,
                values.clone(),
                counts.clone(),
            )))
        }
947
        _ => Err(RuleNotApplicable),
    }
1444
}
/// Lowers `allDifferentExcept` to Minion's `gccweak`, matching Savile Row.
#[register_rule("Minion", 4100, [AllDifferentExcept])]
1379
fn alldifferent_except_to_gccweak(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
1379
    let Expr::AllDifferentExcept(_, matrix, except) = expr else {
1253
        return Err(RuleNotApplicable);
    };
126
    let es = (**matrix).clone().into_list().ok_or(RuleNotApplicable)?;
56
    if es.is_empty() {
        return Err(RuleNotApplicable);
56
    }
56
    let Lit::Int(except_val) = eval_constant(except.as_ref()).ok_or(RuleNotApplicable)? else {
        return Err(RuleNotApplicable);
    };
21
    let domain = es[0]
21
        .domain_of()
21
        .ok_or(RuleNotApplicable)?
        .resolve()
        .map_err(|_| RuleNotApplicable)?;
21
    let GroundDomain::Int(_, _) = domain.as_ref() else {
        return Err(RuleNotApplicable);
    };
21
    let values: Vec<i32> = domain
21
        .values()
        .map_err(|_| RuleNotApplicable)?
182
        .filter_map(|v| match v {
182
            Lit::Int(i) if i != except_val => Some(i),
21
            _ => None,
182
        })
        .collect();
21
    let mut symbols = symbols.clone();
21
    let mut count_exprs = vec![];
21
    for _ in &values {
161
        let decl = symbols.gen_find_auxiliary(&Domain::bool());
161
        count_exprs.push(Expr::Atomic(
161
            Metadata::new(),
161
            Atom::Reference(Reference::new(decl)),
161
        ));
    }
21
    let value_exprs = values
21
        .iter()
161
        .map(|v| Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Int(*v))))
        .collect_vec();
21
    Ok(RuleEffect::new(
21
        Expr::GccWeak(
21
            Metadata::new(),
21
            Moo::new(into_matrix_expr![es]),
21
            Moo::new(into_matrix_expr![value_exprs]),
21
            Moo::new(into_matrix_expr![count_exprs]),
21
        ),
21
        vec![],
21
        symbols,
21
    ))
1379
}
/// A variable exception uses defined occurrence counts, even beneath reification.
#[register_rule("Minion", 4100, [AllDifferentExcept])]
1379
fn alldifferent_variable_except(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
1379
    let Expr::AllDifferentExcept(_, matrix, except) = expr else {
1253
        return Err(RuleNotApplicable);
    };
126
    if eval_constant(except).is_some() || !except.domain_of().is_some_and(|domain| domain.is_int())
    {
56
        return Err(RuleNotApplicable);
70
    }
70
    let inputs = matrix.unwrap_list_cow().ok_or(RuleNotApplicable)?;
35
    if inputs.len() < 2 {
        return Ok(RuleEffect::pure(true.into()));
35
    }
35
    let size = i32::try_from(inputs.len()).map_err(|_| RuleNotApplicable)?;
35
    let mut values = std::collections::BTreeSet::new();
    // Large domains use pairwise comparisons rather than enumerating every value.
35
    let limit = inputs.len().saturating_mul(inputs.len());
35
    for input in inputs.iter() {
105
        let domain = input
105
            .domain_of()
105
            .ok_or(RuleNotApplicable)?
            .resolve()
            .map_err(|_| RuleNotApplicable)?;
105
        if !matches!(domain.as_ref(), GroundDomain::Int(_, _)) {
            return Err(RuleNotApplicable);
105
        }
105
        for value in domain.values().map_err(|_| RuleNotApplicable)? {
287
            let Lit::Int(value) = value else {
                return Err(RuleNotApplicable);
            };
287
            values.insert(value);
287
            if values.len() > limit {
                let mut pairs = Vec::new();
                for (index, left) in inputs.iter().enumerate() {
                    for right in &inputs[index + 1..] {
                        pairs.push(Expr::Or(
                            Metadata::new(),
                            Moo::new(into_matrix_expr![vec![
                                Expr::Neq(
                                    Metadata::new(),
                                    Moo::new(left.clone()),
                                    Moo::new(right.clone())
                                ),
                                Expr::Eq(Metadata::new(), Moo::new(left.clone()), except.clone()),
                            ]]),
                        ));
                    }
                }
                return Ok(RuleEffect::pure(Expr::And(
                    Metadata::new(),
                    Moo::new(into_matrix_expr![pairs]),
                )));
287
            }
        }
    }
35
    let mut symbols = symbols.clone();
35
    let mut counts = Vec::new();
35
    let mut bounds = Vec::new();
35
    let mut literals = Vec::new();
35
    for value in values {
        let count: Expr =
105
            Reference::new(symbols.gen_find_auxiliary(&Domain::int(vec![Range::Bounded(0, size)])))
105
                .into();
105
        let value: Expr = value.into();
105
        bounds.push(Expr::Or(
105
            Metadata::new(),
105
            Moo::new(into_matrix_expr![vec![
105
                Expr::Eq(Metadata::new(), except.clone(), Moo::new(value.clone())),
105
                Expr::Leq(Metadata::new(), Moo::new(count.clone()), Moo::new(1.into())),
105
            ]]),
105
        ));
105
        counts.push(count);
105
        literals.push(value);
    }
35
    let definition = Expr::GccWeak(
35
        Metadata::new(),
35
        Moo::new(into_matrix_expr![inputs.into_owned()]),
35
        Moo::new(into_matrix_expr![literals]),
35
        Moo::new(into_matrix_expr![counts]),
35
    );
35
    Ok(RuleEffect::new(
35
        Expr::And(Metadata::new(), Moo::new(into_matrix_expr![bounds])),
35
        vec![definition],
35
        symbols,
35
    ))
1379
}
/// Native element requires a valid index; materialise identity entries across the finite bounds.
#[register_rule("Minion", 19500, [ElementId])]
6041
fn total_identity_element(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
6041
    let Expr::ElementId(_, matrix, index) = expr else {
5838
        return Err(RuleNotApplicable);
    };
203
    let (entries, labels) = (**matrix)
203
        .clone()
203
        .unwrap_matrix_unchecked()
203
        .ok_or(RuleNotApplicable)?;
175
    if entries.is_empty() {
        let default = if index.return_type() == ReturnType::Bool {
            Expr::ToInt(Metadata::new(), index.clone())
        } else {
            (**index).clone()
        };
        return Ok(RuleEffect::pure(default));
175
    }
175
    if entries
175
        .iter()
350
        .any(|entry| !matches!(entry.return_type(), ReturnType::Int | ReturnType::Bool))
    {
        return Err(RuleNotApplicable);
175
    }
175
    let labels = if matrix.is_list() {
        (1..=entries.len())
            .map(|label| i32::try_from(label).ok())
            .collect::<Option<Vec<_>>>()
            .ok_or(RuleNotApplicable)?
    } else {
175
        labels
175
            .resolve()
            .map_err(|_| RuleNotApplicable)?
            .values()
            .map_err(|_| RuleNotApplicable)?
350
            .map(|label| match label {
350
                Lit::Int(value) => Some(value),
                Lit::Bool(value) => Some(i32::from(value)),
                _ => None,
350
            })
            .collect::<Option<Vec<_>>>()
175
            .ok_or(RuleNotApplicable)?
    };
175
    if labels.len() != entries.len() {
        return Err(RuleNotApplicable);
175
    }
175
    let domain = index
175
        .domain_of()
175
        .ok_or(RuleNotApplicable)?
        .resolve()
        .map_err(|_| RuleNotApplicable)?;
175
    let (minimum, maximum, index) = match domain.as_ref() {
        GroundDomain::Bool => (0, 1, Expr::ToInt(Metadata::new(), index.clone())),
        GroundDomain::Int(ranges, _) => {
175
            let bounds = ranges
175
                .iter()
175
                .map(|range| match range {
                    Range::Single(value) => Some((*value, *value)),
175
                    Range::Bounded(lower, upper) => Some((*lower, *upper)),
                    _ => None,
175
                })
                .collect::<Option<Vec<_>>>()
175
                .ok_or(RuleNotApplicable)?;
            (
175
                bounds
175
                    .iter()
175
                    .map(|(lower, _)| *lower)
                    .min()
175
                    .ok_or(RuleNotApplicable)?,
175
                bounds
175
                    .iter()
175
                    .map(|(_, upper)| *upper)
                    .max()
175
                    .ok_or(RuleNotApplicable)?,
175
                (**index).clone(),
            )
        }
        _ => return Err(RuleNotApplicable),
    };
175
    let entries: HashMap<_, _> = labels.into_iter().zip(entries).collect();
175
    let total = (minimum..=maximum)
875
        .map(|label| {
875
            let entry = entries.get(&label).cloned().unwrap_or_else(|| label.into());
875
            if entry.return_type() == ReturnType::Bool {
                Expr::ToInt(Metadata::new(), Moo::new(entry))
            } else {
875
                entry
            }
875
        })
        .collect::<Vec<_>>();
175
    Ok(RuleEffect::pure(Expr::UnsafeIndex(
175
        Metadata::new(),
175
        Moo::new(into_matrix_expr![total; Domain::int(vec![Range::Bounded(minimum, maximum)])]),
175
        vec![index],
175
    )))
6041
}
/// Introduces `MinionElementOne` from an auxiliary variable bound to `indexOf`.
#[register_rule("Minion", 4400, [AuxDeclaration])]
137467
fn introduce_index_of_from_aux_decl(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
137467
    let Expr::AuxDeclaration(_, reference, inner) = expr else {
2723
        return Err(RuleNotApplicable);
    };
134744
    let Expr::IndexOf(_, matrix, value) = inner.as_ref() else {
133701
        return Err(RuleNotApplicable);
    };
1043
    let value_expr = (**value).clone();
1043
    let value_atom: Atom = value_expr.clone().try_into().or(Err(RuleNotApplicable))?;
539
    let matrix_elems = (**matrix)
539
        .clone()
539
        .unwrap_matrix_unchecked()
        .map(|(elements, _)| elements);
539
    if let Some(elems) = matrix_elems {
539
        let mut atom_list = vec![];
539
        for elem in elems {
2324
            let Expr::Atomic(_, elem) = elem else {
                return Err(RuleNotApplicable);
            };
2324
            atom_list.push(elem);
        }
        // Neither Minion's native `element` constraint nor its `table` fallback (both used
        // below) can take a compound (e.g. tuple) value as a list entry -- both need a plain
        // int/bool atom per position. When the search list holds compound literals (as
        // `FunctionAsRelation`/`FunctionExplicit`'s own `image()` rules build for a compound
        // function domain), fall back to an explicit per-candidate lookup instead: `reference`
        // (the position) is pinned by a disjunction trying each candidate position, guarded by
        // `value_expr` equalling that candidate's own literal -- the same "compare against each
        // candidate" shape `RelationPacked::tuple_membership_expr` and the tuple-literal equality
        // rules (`types::tuple::horizontal::tuple_literal_eq_literal`) already use for compound
        // values elsewhere, just built directly here since this list is always fully literal.
539
        if let Some(lookup) = compound_index_of_lookup(&atom_list, &value_expr, reference) {
            return Ok(RuleEffect::pure(lookup));
539
        }
        // Use table only if the search value can be out of bounds; otherwise element_one.
539
        if let Some(search_values) = literal_matrix_int_values(&atom_list)
539
            && index_of_value_may_be_outside_matrix(&value_expr, &search_values)
        {
7
            if let Ok(atom_list) = indexed_index_of_inverse_lookup(matrix.as_ref(), &value_expr) {
7
                return Ok(RuleEffect::pure(Expr::MinionElementOne(
7
                    Metadata::new(),
7
                    atom_list,
7
                    Moo::new(Atom::Reference(reference.clone())),
7
                    Moo::new(value_atom),
7
                )));
            }
            let rows = index_of_table_rows(&value_expr, &search_values)?;
            let tuple = into_matrix_expr![vec![
                value_expr,
                Expr::Atomic(Metadata::new(), Atom::Reference(reference.clone()),),
            ]];
            return Ok(RuleEffect::pure(Expr::Table(
                Metadata::new(),
                Moo::new(tuple),
                Moo::new(rows),
            )));
532
        }
532
        let atom_list = pad_indexed_index_of_list(matrix.as_ref(), atom_list.clone(), reference)
            .or_else(|| pad_represented_index_of_list(atom_list.clone(), reference))
532
            .unwrap_or(atom_list);
532
        return Ok(RuleEffect::pure(Expr::MinionElementOne(
532
            Metadata::new(),
532
            atom_list,
532
            Moo::new(Atom::Reference(reference.clone())),
532
            Moo::new(value_atom),
532
        )));
    }
    let atom_list = indexed_index_of_inverse_lookup(matrix.as_ref(), &value_expr)?;
    Ok(RuleEffect::pure(Expr::MinionElementOne(
        Metadata::new(),
        atom_list,
        Moo::new(value_atom),
        Moo::new(Atom::Reference(reference.clone())),
    )))
137467
}
/// `indexOf([e1,...,en], value) = reference` where at least one `ei` is a *compound* literal
/// (a matrix of plain int/bool literals is left to the native `element`/`table` lowering, which
/// already handles it): `reference` is pinned to the one-based position `i` such that `value`
/// equals `ei`, expressed directly as a disjunction over positions rather than through any
/// Minion-specific "list of atoms" primitive, since a compound element has no single atom to put
/// in such a list. `value`'s own equality against a compound literal is resolved by the ordinary
/// tuple-literal-equality machinery once this reaches the general rule engine again (this rule
/// only ever fires after the whole model has already been rewritten down to the `Minion`
/// ruleset's own passes, but the `Eq`/`In` nodes built here are still plain AST nodes, not
/// Minion-specific ones, so earlier "Base"/"ReprGeneral" rules such as
/// `types::tuple::horizontal::tuple_literal_eq_literal` still apply to them on the next pass).
///
/// Returns `None` (not an error) when every element is already scalar, so the caller falls
/// through to the existing, more efficient native-constraint paths unchanged.
539
fn compound_index_of_lookup(
539
    atom_list: &[Atom],
539
    value_expr: &Expr,
539
    reference: &Reference,
539
) -> Option<Expr> {
539
    if literal_matrix_int_values(atom_list).is_some() {
539
        return None;
    }
    let literals: Vec<&Lit> = atom_list
        .iter()
        .map(|atom| match atom {
            Atom::Literal(lit) => Some(lit),
            Atom::Reference(_) => None,
        })
        .collect::<Option<_>>()?;
    let reference_expr = Expr::Atomic(Metadata::new(), Atom::Reference(reference.clone()));
    let choices: Vec<Expr> = literals
        .into_iter()
        .enumerate()
        .map(|(zero_based_index, lit)| {
            let position = zero_based_index as i32 + 1;
            let matches_value = Expr::Eq(
                Metadata::new(),
                Moo::new(value_expr.clone()),
                Moo::new(Expr::Atomic(Metadata::new(), Atom::Literal(lit.clone()))),
            );
            let pins_position = Expr::Eq(
                Metadata::new(),
                Moo::new(reference_expr.clone()),
                Moo::new(position.into()),
            );
            Expr::And(
                Metadata::new(),
                Moo::new(matrix_expr![matches_value, pins_position]),
            )
        })
        .collect();
    Some(Expr::Or(
        Metadata::new(),
        Moo::new(into_matrix_expr![choices]),
    ))
539
}
7
fn indexed_index_of_inverse_lookup(
7
    matrix_expr: &Expr,
7
    value_expr: &Expr,
7
) -> Result<Vec<Atom>, ApplicationError> {
    let (elems, index_domain) =
7
        indexed_index_of_literal_parts(matrix_expr).ok_or(RuleNotApplicable)?;
7
    let GroundDomain::Int(index_ranges, _) = index_domain.as_ref() else {
        return Err(RuleNotApplicable);
    };
7
    let index_values = Range::values(index_ranges)
7
        .ok_or(RuleNotApplicable)?
        .collect_vec();
7
    if index_values.len() != elems.len() {
        return Err(RuleNotApplicable);
7
    }
7
    let value_domain = value_expr.domain_of().ok_or(RuleNotApplicable)?;
7
    let value_domain = value_domain.resolve().map_err(|_| RuleNotApplicable)?;
7
    let GroundDomain::Int(value_ranges, _) = value_domain.as_ref() else {
        return Err(RuleNotApplicable);
    };
7
    let value_values = Range::values(value_ranges)
7
        .ok_or(RuleNotApplicable)?
        .collect_vec();
7
    let max_value = value_values
7
        .iter()
7
        .copied()
7
        .max()
7
        .ok_or(RuleNotApplicable)?;
21
    if value_values.iter().any(|value| *value < 1) {
        return Err(RuleNotApplicable);
7
    }
7
    let mut lookup: Vec<i32> = (1..=max_value).collect();
7
    let mut seen_elements = HashSet::new();
7
    for (index, elem) in index_values.into_iter().zip(elems) {
14
        if elem < 1 || elem > max_value || !seen_elements.insert(elem) {
            return Err(RuleNotApplicable);
14
        }
14
        lookup[(elem - 1) as usize] = index;
    }
7
    Ok(lookup
7
        .into_iter()
21
        .map(|value| Atom::Literal(Lit::Int(value)))
        .collect())
7
}
532
fn pad_indexed_index_of_list(
532
    matrix_expr: &Expr,
532
    atom_list: Vec<Atom>,
532
    result: &Reference,
532
) -> Option<Vec<Atom>> {
532
    let index_domain = match matrix_expr {
        Expr::AbstractLiteral(_, AbstractLiteral::Matrix(_, index_domain)) => {
            index_domain.resolve().ok()?
        }
        Expr::Atomic(
            _,
            Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Matrix(_, index_domain))),
532
        ) => index_domain.clone(),
        _ => return None,
    };
532
    let GroundDomain::Int(index_ranges, _) = index_domain.as_ref() else {
        return None;
    };
532
    let index_values = Range::values(index_ranges)?.collect_vec();
532
    if index_values.len() != atom_list.len() {
        return None;
532
    }
532
    let result_domain = result.domain()?.resolve().ok()?;
532
    let GroundDomain::Int(result_ranges, _) = result_domain.as_ref() else {
        return None;
    };
532
    let max_result = Range::values(result_ranges)?.max()?;
532
    let mut padded: Vec<Atom> = (1..=max_result)
2310
        .map(|value| Lit::Int(value).into())
        .collect();
532
    for (index, atom) in index_values.into_iter().zip(atom_list) {
2310
        if !(1..=max_result).contains(&index) {
            return None;
2310
        }
2310
        padded[(index - 1) as usize] = atom;
    }
532
    Some(padded)
532
}
fn pad_represented_index_of_list(atom_list: Vec<Atom>, result: &Reference) -> Option<Vec<Atom>> {
    let result_domain = result.domain()?.resolve().ok()?;
    let GroundDomain::Int(result_ranges, _) = result_domain.as_ref() else {
        return None;
    };
    let result_values = Range::values(result_ranges)?.collect_vec();
    let max_result = result_values.iter().copied().max()?;
    if max_result <= atom_list.len() as i32 {
        return None;
    }
    let mut padded: Vec<Atom> = (1..=max_result)
        .map(|value| Lit::Int(value).into())
        .collect();
    let mut changed = false;
    for atom in atom_list {
        let position = represented_matrix_to_atom_index(&atom)?;
        if !(1..=max_result).contains(&position) {
            return None;
        }
        padded[(position - 1) as usize] = atom;
        changed = true;
    }
    changed.then_some(padded)
}
fn represented_matrix_to_atom_index(atom: &Atom) -> Option<i32> {
    let Atom::Reference(reference) = atom else {
        return None;
    };
    let name = reference.name();
    let Name::Represented(fields) = &*name else {
        return None;
    };
    let (_, rule, suffix) = fields.as_ref();
    if *rule != MatrixComponents::id() {
        return None;
    }
    suffix.as_str().parse().ok()
}
7
fn indexed_index_of_literal_parts(matrix_expr: &Expr) -> Option<(Vec<i32>, Moo<GroundDomain>)> {
7
    match matrix_expr {
        Expr::AbstractLiteral(_, AbstractLiteral::Matrix(elems, index_domain)) => Some((
            elems.iter().map(expr_int_literal).collect::<Option<_>>()?,
            index_domain.resolve().ok()?,
        )),
        Expr::Atomic(
            _,
            Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Matrix(elems, index_domain))),
        ) => Some((
7
            elems.iter().map(lit_int).collect::<Option<_>>()?,
7
            index_domain.clone(),
        )),
        _ => None,
    }
7
}
fn expr_int_literal(expr: &Expr) -> Option<i32> {
    match expr {
        Expr::Atomic(_, Atom::Literal(lit)) => lit_int(lit),
        _ => None,
    }
}
4662
fn lit_int(lit: &Lit) -> Option<i32> {
4662
    match lit {
4662
        Lit::Int(value) => Some(*value),
        _ => None,
    }
4662
}
1078
fn literal_matrix_int_values(atoms: &[Atom]) -> Option<Vec<i32>> {
1078
    atoms
1078
        .iter()
4648
        .map(|atom| match atom {
4648
            Atom::Literal(lit) => lit_int(lit),
            _ => None,
4648
        })
        .collect()
1078
}
539
fn index_of_value_may_be_outside_matrix(value_expr: &Expr, search_values: &[i32]) -> bool {
539
    let Some(value_domain) = value_expr
539
        .domain_of()
539
        .and_then(|domain| domain.resolve().ok())
    else {
        return true;
    };
539
    let Ok(value_values) = value_domain.values_i32() else {
        return true;
    };
539
    value_values
539
        .iter()
2331
        .any(|value| !search_values.contains(value))
539
}
fn index_of_table_rows(value_expr: &Expr, search_values: &[i32]) -> Result<Expr, ApplicationError> {
    let value_domain = value_expr
        .domain_of()
        .ok_or(RuleNotApplicable)?
        .resolve()
        .map_err(|_| RuleNotApplicable)?;
    let value_values = value_domain.values_i32().map_err(|_| RuleNotApplicable)?;
    let sentinel = i32::try_from(search_values.len()).map_err(|_| RuleNotApplicable)? + 1;
    let value_to_index: HashMap<i32, i32> = search_values
        .iter()
        .enumerate()
        .map(|(index, value)| (*value, (index + 1) as i32))
        .collect();
    let rows = value_values
        .into_iter()
        .map(|value| {
            let index = value_to_index.get(&value).copied().unwrap_or(sentinel);
            into_matrix_expr![vec![
                Expr::Atomic(Metadata::new(), Lit::Int(value).into()),
                Expr::Atomic(Metadata::new(), Lit::Int(index).into()),
            ]]
        })
        .collect_vec();
    Ok(into_matrix_expr![rows])
}
/// General (any-`Literal`) counterpart to [`indexed_index_of_literal_parts`], used when the
/// search target may itself be compound (e.g. a tuple), not just a plain int.
758
fn indexed_index_of_parts(matrix_expr: &Expr) -> Option<(Vec<Lit>, Moo<GroundDomain>)> {
758
    match matrix_expr {
        Expr::AbstractLiteral(_, AbstractLiteral::Matrix(elems, index_domain)) => Some((
2
            elems.iter().map(eval_constant).collect::<Option<_>>()?,
2
            index_domain.resolve().ok()?,
        )),
        Expr::Atomic(
            _,
            Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Matrix(elems, index_domain))),
756
        ) => Some((elems.clone(), index_domain.clone())),
        _ => None,
    }
758
}
2557
fn fold_constant_index_of_to_index(index_of: &Expr) -> Option<Expr> {
2557
    let Expr::IndexOf(_, matrix, value) = index_of else {
1799
        return None;
    };
758
    let search_value = eval_constant(value.as_ref())?;
758
    let (elems, index_domain) = indexed_index_of_parts(matrix.as_ref())?;
758
    let GroundDomain::Int(index_ranges, _) = index_domain.as_ref() else {
        return None;
    };
758
    let index_values = Range::values(index_ranges)?.collect_vec();
758
    if elems.len() != index_values.len() {
        return None;
758
    }
758
    for (elem, index) in elems.into_iter().zip(index_values) {
2022
        if elem.essence_cmp(&search_value).is_eq() {
758
            return Some(Expr::Atomic(
758
                Metadata::new(),
758
                Atom::Literal(Lit::Int(index)),
758
            ));
1264
        }
    }
    // Match the identity-padding used by [`pad_indexed_index_of_list`] / Minion `element_one`
    // for partial permutations (e.g. `indexOf([j,i;int(i,j)], k)`): values absent from the
    // matrix map to themselves as indices. Without this, out-of-matrix lookups become unconstrained
    // auxiliaries and lex constraints are weakened. Only meaningful for a plain int value -- a
    // compound value has no "map to itself as an index" reading, so it's left unfolded (the
    // AuxDeclaration-based path, `compound_index_of_lookup`, still handles it structurally).
    if let Lit::Int(search_value) = search_value {
        return Some(Expr::Atomic(
            Metadata::new(),
            Atom::Literal(Lit::Int(search_value)),
        ));
    }
    None
2557
}
/// Introduces an auxiliary variable for `indexOf` used as a matrix index.
#[register_rule("Minion", 4300, [SafeIndex])]
9671
fn flatten_index_of_index(expr: &Expr, _symbols: &SymbolTable) -> ApplicationResult {
9671
    let Expr::SafeIndex(meta, subject, indices) = expr else {
2489
        return Err(RuleNotApplicable);
    };
7182
    if indices.len() != 1 {
        return Err(RuleNotApplicable);
7182
    }
7182
    let Expr::IndexOf(_, _, _) = indices[0].clone() else {
6426
        return Err(RuleNotApplicable);
    };
756
    if let Some(index) = fold_constant_index_of_to_index(&indices[0]) {
756
        return Ok(RuleEffect::pure(Expr::SafeIndex(
756
            meta.clone(),
756
            subject.clone(),
756
            vec![index],
756
        )));
    }
    defer_aux_var(&indices[0], {
        let meta = meta.clone();
        let subject = subject.clone();
        move |aux_var_info| {
            RuleEffect::new(
                Expr::SafeIndex(meta.clone(), subject.clone(), vec![aux_var_info.as_expr()]),
                vec![aux_var_info.top_level_expr()],
                aux_var_info.symbols(),
            )
        }
    })
    .ok_or(RuleNotApplicable)
9671
}
/// Introduces a Minion `MinusEq` constraint from `x = -y`, where x and y are atoms.
///
/// ```text
/// x = -y ~> MinusEq(x,y)
///
///   where x,y are atoms
/// ```
#[register_rule("Minion", 4400, [Eq])]
152285
fn introduce_minuseq_from_eq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
152285
    let Expr::Eq(_, a, b) = expr else {
2741
        return Err(RuleNotApplicable);
    };
299067
    fn try_get_atoms(a: &Moo<Expr>, b: &Moo<Expr>) -> Option<(Atom, Atom)> {
299067
        let a: &Atom = (&**a).try_into().ok()?;
253085
        let Expr::Neg(_, b) = &**b else {
253050
            return None;
        };
35
        let b: &Atom = b.try_into().ok()?;
28
        Some((a.clone(), b.clone()))
299067
    }
    // x = - y. Find this symmetrically (a = - b or b = -a)
149544
    let Some((x, y)) = try_get_atoms(a, b).or_else(|| try_get_atoms(b, a)) else {
149516
        return Err(RuleNotApplicable);
    };
28
    Ok(RuleEffect::pure(Expr::FlatMinusEq(
28
        Metadata::new(),
28
        Moo::new(x),
28
        Moo::new(y),
28
    )))
152285
}
/// Introduces a Minion `MinusEq` constraint from `x =aux -y`, where x and y are atoms.
///
/// ```text
/// x =aux -y ~> MinusEq(x,y)
///
///   where x,y are atoms
/// ```
#[register_rule("Minion", 4400, [AuxDeclaration])]
137467
fn introduce_minuseq_from_aux_decl(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
    // a =aux -b
    //
137467
    let Expr::AuxDeclaration(_, reference, b) = expr else {
2723
        return Err(RuleNotApplicable);
    };
134744
    let a = Atom::Reference(reference.clone());
134744
    let Expr::Neg(_, b) = (**b).clone() else {
134317
        return Err(RuleNotApplicable);
    };
427
    let Ok(b) = b.try_into() else {
7
        return Err(RuleNotApplicable);
    };
420
    Ok(RuleEffect::pure(Expr::FlatMinusEq(
420
        Metadata::new(),
420
        Moo::new(a),
420
        Moo::new(b),
420
    )))
137467
}
/// Converts an implication to either `ineq` or `reifyimply`
///
/// ```text
/// x -> y ~> ineq(x,y,0)
/// where x is atomic, y is atomic
///
/// x -> y ~> reifyimply(y,x)
/// where x is atomic, y is non-atomic
/// ```
#[register_rule("Minion", 4400, [Imply])]
46933
fn introduce_reifyimply_ineq_from_imply(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
46933
    let Expr::Imply(_, x, y) = expr else {
2765
        return Err(RuleNotApplicable);
    };
44168
    let x_atom: &Atom = x.as_ref().try_into().or(Err(RuleNotApplicable))?;
    // if both x and y are atoms,  x -> y ~> ineq(x,y,0)
    //
    // if only x is an atom, x -> y ~> reifyimply(y,x)
20308
    if let Ok(y_atom) = TryInto::<&Atom>::try_into(y.as_ref()) {
1548
        Ok(RuleEffect::pure(Expr::FlatIneq(
1548
            Metadata::new(),
1548
            Moo::new(x_atom.clone()),
1548
            Moo::new(y_atom.clone()),
1548
            Box::new(0.into()),
1548
        )))
    } else {
18760
        Ok(RuleEffect::pure(Expr::MinionReifyImply(
18760
            Metadata::new(),
18760
            y.clone(),
18760
            x_atom.clone(),
18760
        )))
    }
46933
}
/// Constant-folds `__inDomain(indexOf(...), domain)` when the indexOf result domain is
/// already contained in `domain`.
#[register_rule("Minion", 4350, [InDomain])]
2510
fn constant_fold_indomain_index_of(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
2510
    let Expr::InDomain(_, e, domain) = expr else {
2489
        return Err(RuleNotApplicable);
    };
21
    let Expr::IndexOf(..) = e.as_ref() else {
21
        return Err(RuleNotApplicable);
    };
    let expr_domain = e
        .domain_of()
        .ok_or(RuleNotApplicable)?
        .resolve()
        .map_err(|_| RuleNotApplicable)?;
    let domain = domain.resolve().map_err(|_| RuleNotApplicable)?;
    let intersection = expr_domain
        .intersect(&domain)
        .map_err(|_| RuleNotApplicable)?;
    let GroundDomain::Int(_, _) = expr_domain.as_ref() else {
        return Err(RuleNotApplicable);
    };
    let GroundDomain::Int(_, _) = &intersection else {
        return Err(RuleNotApplicable);
    };
    if normalise_int_domain(&intersection) == normalise_int_domain(expr_domain.as_ref()) {
        return Ok(RuleEffect::pure(true.into()));
    }
    if let Ok(values) = intersection.values_i32()
        && values.is_empty()
    {
        return Ok(RuleEffect::pure(false.into()));
    }
    Err(RuleNotApplicable)
2510
}
fn normalise_int_domain(domain: &GroundDomain) -> GroundDomain {
    match domain {
        GroundDomain::Int(ranges, representation) => GroundDomain::Int(
            Range::squeeze(
                &ranges
                    .iter()
                    .map(|range| Range::new(range.low().copied(), range.high().copied()))
                    .collect_vec(),
            ),
            representation.clone(),
        ),
        _ => domain.clone(),
    }
}
/// Resolves `__n =aux matrix[index]` when `index` is a constant `indexOf` lookup.
#[register_rule("Minion", 4450, [AuxDeclaration])]
137743
fn resolve_safeindex_index_of_aux(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
137743
    let Expr::AuxDeclaration(meta, reference, inner) = expr else {
2993
        return Err(RuleNotApplicable);
    };
134750
    let Expr::SafeIndex(index_meta, subject, indices) = inner.as_ref() else {
132951
        return Err(RuleNotApplicable);
    };
1799
    if indices.len() != 1 {
        return Err(RuleNotApplicable);
1799
    }
1799
    let index = fold_constant_index_of_to_index(&indices[0]).ok_or(RuleNotApplicable)?;
    let safeindex = Expr::SafeIndex(index_meta.clone(), subject.clone(), vec![index]);
    let RuleEffect { new_expression, .. } = try_index_matrix_components(&safeindex, symbols)?;
    Ok(RuleEffect::pure(Expr::Eq(
        meta.clone(),
        Moo::new(Expr::Atomic(
            Metadata::new(),
            Atom::Reference(reference.clone()),
        )),
        Moo::new(new_expression),
    )))
137743
}
/// Rewrites `__n =aux matrix[indexOf(..., k)]` when constant `k` is absent from the matrix.
///
/// Uses the same identity-index fallback as [`fold_constant_index_of_to_index`]: the lookup
/// becomes `matrix[k]`. Previously this dropped the aux link entirely, leaving unconstrained
/// auxiliaries inside lex constraints.
#[register_rule("Minion", 4450, [AuxDeclaration])]
137743
fn drop_invalid_constant_index_of_safeindex_aux(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
137743
    let Expr::AuxDeclaration(meta, reference, inner) = expr else {
2993
        return Err(RuleNotApplicable);
    };
134750
    let Expr::SafeIndex(index_meta, subject, indices) = inner.as_ref() else {
132951
        return Err(RuleNotApplicable);
    };
1799
    if indices.len() != 1 {
        return Err(RuleNotApplicable);
1799
    }
1799
    let Expr::IndexOf(_, _, value) = &indices[0] else {
1799
        return Err(RuleNotApplicable);
    };
    // Prefer the shared folder (including identity padding). If it succeeds, rewrite to a plain
    // SafeIndex with that constant index; flatten_index_of_index / resolve paths usually handle
    // this earlier, so this is a fallback.
    let Some(index) = fold_constant_index_of_to_index(&indices[0]) else {
        return Err(RuleNotApplicable);
    };
    if expr_int_literal(value.as_ref()).is_none() {
        return Err(RuleNotApplicable);
    }
    Ok(RuleEffect::pure(Expr::AuxDeclaration(
        meta.clone(),
        reference.clone(),
        Moo::new(Expr::SafeIndex(
            index_meta.clone(),
            subject.clone(),
            vec![index],
        )),
    )))
137743
}
/// Converts `__inDomain(a,domain) to w-inintervalset.
///
/// This applies if domain is integer and finite.
#[register_rule("Minion", 4400, [InDomain])]
2947
fn introduce_wininterval_set_from_indomain(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
2947
    let Expr::InDomain(_, e, domain) = expr else {
2807
        return Err(RuleNotApplicable);
    };
140
    let Expr::Atomic(_, atom @ Atom::Reference(_)) = e.as_ref() else {
21
        return Err(RuleNotApplicable);
    };
119
    let Some(ranges) = domain.as_int_ground() else {
        return Err(RuleNotApplicable);
    };
119
    let mut out_ranges = vec![];
119
    for range in ranges {
175
        match range {
            Range::Single(x) => {
84
                out_ranges.push(*x);
84
                out_ranges.push(*x);
            }
            Range::Bounded(x, y) => {
91
                out_ranges.push(*x);
91
                out_ranges.push(*y);
            }
            Range::UnboundedR(_) | Range::UnboundedL(_) | Range::Unbounded => {
                return Err(RuleNotApplicable);
            }
        }
    }
119
    Ok(RuleEffect::pure(Expr::MinionWInIntervalSet(
119
        Metadata::new(),
119
        atom.clone(),
119
        out_ranges,
119
    )))
2947
}
/// Converts `[....][i]` to `element_one` if:
///
/// 1. the subject is a list literal
/// 2. the subject is one dimensional
#[register_rule(
    "Minion",
    4400,
    [Eq / SafeIndex, AuxDeclaration / SafeIndex, SafeIndex]
)]
19294
fn introduce_element_from_index(expr: &Expr, symtab: &SymbolTable) -> ApplicationResult {
19294
    let (equalto, subject, indices) = match expr.clone() {
6884
        Expr::Eq(_, e1, e2) => match (Moo::unwrap_or_clone(e1), Moo::unwrap_or_clone(e2)) {
            (Expr::Atomic(_, eq), Expr::SafeIndex(_, subject, indices)) => {
21
                Ok((eq, subject, indices))
            }
            (Expr::SafeIndex(_, subject, indices), Expr::Atomic(_, eq)) => {
5369
                Ok((eq, subject, indices))
            }
1494
            _ => Err(RuleNotApplicable),
        },
1883
        Expr::AuxDeclaration(_, reference, expr) => match Moo::unwrap_or_clone(expr) {
            Expr::SafeIndex(_, subject, indices) => {
1799
                Ok((Atom::Reference(reference), subject, indices))
            }
84
            _ => Err(RuleNotApplicable),
        },
7870
        Expr::SafeIndex(_, subject, indices) if expr.return_type() == ReturnType::Bool => {
1388
            Ok((Atom::Literal(Lit::Bool(true)), subject, indices))
        }
9139
        _ => Err(RuleNotApplicable),
10717
    }?;
8577
    if indices.len() != 1 {
        return Err(RuleNotApplicable);
8577
    }
    // Minion's `element` is 1-based. A list already is, so it needs no shift; any other contiguous
    // integer index domain does, which is why this rule used to decline for e.g.
    // `[10,20,30,40; int(0..3)][i]`.
8577
    let subject = Moo::unwrap_or_clone(subject);
8577
    let mut symtab = symtab.clone();
8577
    let mut top_level_exprs = vec![];
8577
    let (list, index) = match subject.clone().into_list() {
        Some(list) => {
8325
            let Expr::Atomic(_, index) = indices[0].clone() else {
3213
                return Err(RuleNotApplicable);
            };
5112
            (list, index)
        }
        None => {
252
            let Some((list, index_domain)) = subject.unwrap_matrix_unchecked() else {
                return Err(RuleNotApplicable);
            };
252
            let offset = offset_to_one_based(&index_domain, list.len()).ok_or(RuleNotApplicable)?;
252
            if offset == 0 {
                // Already 1-based; use the index as it stands rather than minting an auxiliary
                // for `index + 0`.
217
                let Expr::Atomic(_, index) = indices[0].clone() else {
                    return Err(RuleNotApplicable);
                };
217
                (list, index)
            } else {
35
                let index_expr = Expr::Sum(
35
                    Metadata::new(),
35
                    Moo::new(into_matrix_expr![vec![
35
                        indices[0].clone(),
35
                        Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Int(offset))),
35
                    ]]),
35
                );
                let index =
35
                    flatten_expression_to_atom(index_expr, &mut symtab, &mut top_level_exprs)?;
35
                (list, index)
            }
        }
    };
5364
    let mut atom_list = vec![];
5364
    for elem in list {
127082
        let Expr::Atomic(_, elem) = elem else {
63
            return Err(RuleNotApplicable);
        };
127019
        atom_list.push(elem);
    }
5301
    Ok(RuleEffect::new(
5301
        Expr::MinionElementOne(
5301
            Metadata::new(),
5301
            atom_list,
5301
            Moo::new(index),
5301
            Moo::new(equalto),
5301
        ),
5301
        top_level_exprs,
5301
        symtab,
5301
    ))
19294
}
/// How much to add to an Essence index to reach Minion's 1-based `element` position.
///
/// `None` when the index domain is not a contiguous integer range covering exactly the elements,
/// in which case the caller has no straightforward shift to apply.
252
fn offset_to_one_based(index_domain: &DomainPtr, elements: usize) -> Option<i32> {
252
    let resolved = index_domain.resolve().ok()?;
252
    let GroundDomain::Int(ranges, _) = resolved.as_ref() else {
        return None;
    };
252
    if !Range::is_contiguous(ranges) {
        return None;
252
    }
252
    let low = Range::low_of(ranges)?;
252
    let values = Range::values(ranges)?.count();
252
    (values == elements).then_some(1 - low)
252
}
/// Flattens an implication.
///
/// ```text
/// e -> y  (where e is non atomic)
///  ~~>
/// __0 -> y,
///
/// with new top level constraints
/// __0 =aux x
///
/// ```
///
/// Unlike other expressions, only the left hand side of implications are flattened. This is
/// because implications can be expressed as a `reifyimply` constraint, which takes a constraint as
/// an argument:
///
/// ``` text
/// r -> c ~> refifyimply(r,c)
///  where r is an atom, c is a constraint
/// ```
///
/// See [`introduce_reifyimply_ineq_from_imply`].
#[register_rule("Minion", 4200, [Imply])]
17116
fn flatten_imply(expr: &Expr, _symbols: &SymbolTable) -> ApplicationResult {
17116
    let Expr::Imply(meta, x, y) = expr else {
1595
        return Err(RuleNotApplicable);
    };
    // flatten x
15521
    defer_aux_var(x.as_ref(), {
15521
        let meta = meta.clone();
15521
        let y = y.clone();
15515
        move |aux_var_info| {
15515
            RuleEffect::new(
15515
                Expr::Imply(meta.clone(), Moo::new(aux_var_info.as_expr()), y.clone()),
15515
                vec![aux_var_info.top_level_expr()],
15515
                aux_var_info.symbols(),
15515
            )
15515
        }
    })
15521
    .ok_or(RuleNotApplicable)
17116
}
#[register_rule("Minion", 4200, [SafeDiv, Neq, SafeMod, SafePow, Leq, Geq, Abs, Neg, Not, SafeIndex, InDomain, ToInt])]
372685
fn flatten_generic(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
372685
    if !matches!(
372685
        expr,
372685
        Expr::SafeDiv(_, _, _)
372685
            | Expr::Neq(_, _, _)
372685
            | Expr::SafeMod(_, _, _)
372685
            | Expr::SafePow(_, _, _)
372685
            | Expr::Leq(_, _, _)
372685
            | Expr::Geq(_, _, _)
372685
            | Expr::Abs(_, _)
372685
            | Expr::Neg(_, _)
372685
            | Expr::Not(_, _)
372685
            | Expr::SafeIndex(_, _, _)
372685
            | Expr::InDomain(_, _, _)
372685
            | Expr::ToInt(_, _)
372685
    ) {
1439
        return Err(RuleNotApplicable);
371246
    }
    // Cloning the model-wide symbol table costs a clone per declaration, and most calls reach
    // this rule with already-atomic children. Decide applicability from the child domains
    // first, and hand those domains to the rewrite so they are only derived once.
    let (expr, new_tops, symbols) =
371246
        flatten_children_to_aux_vars(expr, symbols, |needs_aux, _| needs_aux > 0)
371246
            .ok_or(RuleNotApplicable)?;
83910
    Ok(RuleEffect::new(expr, new_tops, symbols))
372685
}
#[register_rule("Minion", 4200, [Eq])]
36794
fn flatten_eq(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
36794
    if !matches!(expr, Expr::Eq(_, _, _)) {
1571
        return Err(RuleNotApplicable);
35223
    }
    // eq: both sides have to be non flat for the rule to be applicable. Deciding that from the
    // child domains keeps the symbol-table clone off the failure path.
    let (expr, new_tops, symbols) =
35223
        flatten_children_to_aux_vars(expr, symbols, |needs_aux, num_children| {
35223
            num_children == 2 && needs_aux == 2
35223
        })
35223
        .ok_or(RuleNotApplicable)?;
1165
    Ok(RuleEffect::new(expr, new_tops, symbols))
36794
}
/// Flattens products containing lists.
///
/// For example,
///
/// ```plain
/// product([|x|,y,z]) ~~> product([aux1,y,z]), aux1=|x|
/// ```
#[register_rule("Minion", 4200, [Product])]
1778
fn flatten_product(expr: &Expr, symtab: &SymbolTable) -> ApplicationResult {
    // product cannot use flatten_generic as we don't want to put the immediate child in an aux
    // var, as that is the matrix literal. Instead we want to put the children of the matrix
    // literal in an aux var.
    //
    // e.g.
    //
    // flatten_generic would do
    //
    // product([|x|,y,z]) ~~> product(aux1), aux1=[x,y,z]
    //
    // we want to do
    //
    // product([|x|,y,z]) ~~> product([aux1,y,z]), aux1=|x|
    //
    // We only want to flatten products containing matrix literals that are lists but not child terms, e.g.
    //
    //  product(x[1,..]) ~~> product(aux1),aux1 = x[1,..].
    //
    //  Instead, we let the representation and vertical rules for matrices turn x[1,..] into a
    //  matrix literal.
    //
    //  product(x[1,..]) ~~ slice_matrix_components ~~> product([x11,x12,x13,x14])
1778
    let Expr::Product(_, factors) = expr else {
1607
        return Err(RuleNotApplicable);
    };
171
    let factors = (**factors).clone().into_list().ok_or(RuleNotApplicable)?;
171
    let mut new_factors = vec![];
171
    let mut top_level_exprs = vec![];
171
    let mut symtab = symtab.clone();
171
    let mut changed = false;
171
    for factor in factors {
        // Already-flat atoms are left alone. Non-atoms (including toInt(bool),
        // which Minion treats as the bool atom itself) must be rewritten.
342
        let was_atomic = matches!(&factor, Expr::Atomic(_, _));
342
        let atom = flatten_expression_to_atom(factor, &mut symtab, &mut top_level_exprs)?;
342
        if !was_atomic {
111
            changed = true;
231
        }
342
        new_factors.push(Expr::Atomic(Metadata::new(), atom));
    }
    // have we done anything?
    // Aux-vars add top-level declarations; stripping toInt(bool) → bool atom
    // rewrites factors without creating auxes, so also count that as progress.
171
    if !changed && top_level_exprs.is_empty() {
112
        return Err(RuleNotApplicable);
59
    }
59
    let new_expr = Expr::Product(Metadata::new(), Moo::new(into_matrix_expr![new_factors]));
59
    Ok(RuleEffect::new(new_expr, top_level_exprs, symtab))
1778
}
/// Flattens a matrix literal that contains expressions.
///
/// For example,
///
/// ```plain
/// [1,e/2,f*5] ~~> [1,__0,__1],
///
/// where
/// __0 =aux e/2,
/// __1 =aux f*5
/// ```
// Match parents of matrix values whether they appear as `AbstractLiteral`
// nodes or as `Atomic` literals wrapping a matrix.
#[register_rule("Minion", 1000, [* / AbstractLiteral, * / Atomic])] // lower than matrix_to_list
306768
fn flatten_matrix_literal(expr: &Expr, symtab: &SymbolTable) -> ApplicationResult {
    // do not flatten matrix literals inside sum, or, and, product as these expressions either do
    // their own flattening, or do not need flat expressions.
306768
    if matches!(
306768
        expr,
        Expr::And(_, _) | Expr::Or(_, _) | Expr::Sum(_, _) | Expr::Product(_, _)
    ) {
164396
        return Err(RuleNotApplicable);
142372
    }
142372
    let mut symbols = symtab.clone();
142372
    let mut top_level_exprs = vec![];
    // flatten any children that are matrix literals
358121
    let (expr, num_changed) = rewrite_children(expr, |child| {
        // is this a matrix literal?
        //
        // as we arn't changing the number of arguments in the matrix, we can apply this to all
        // matrices, not just those that are lists.
        //
        // this also means that this rule works with n-d matrices -- the inner dimensions of n-d
        // matrices cant be turned into lists, as described the docstring for matrix_to_list.
358121
        let Some((mut es, index_domain)) = child.clone().unwrap_matrix_unchecked() else {
357421
            return (child, false);
        };
700
        let mut child_changed = false;
        // flatten expressions
700
        for e in es.iter_mut() {
3808
            if let Some((reference, top)) = to_aux_var_in(e, &mut symbols) {
154
                *e = Expr::Atomic(Metadata::new(), Atom::Reference(reference));
154
                top_level_exprs.push(top);
154
                child_changed = true;
3654
            } else if let Expr::SafeIndex(_, subject, indices) = e {
                let index_has_index_of = indices
                    .iter()
                    .any(|index| matches!(index, Expr::IndexOf(..)));
                let subject_is_ref = matches!(**subject, Expr::Atomic(_, Atom::Reference(_)));
                if !index_has_index_of && subject_is_ref {
                    continue;
                }
                // we dont normally flatten indexing expressions, but we want to do it if they are
                // inside a matrix list.
                //
                // remove_dimension_from_matrix_indexing turns [[1,2,3],[4,5,6]][i,j]
                // into [[1,2,3][j],[4,5,6][j],[7,8,9][j]][i].
                //
                // we want to flatten this to
                // [__0,__1,__2][i]
                let Some(domain) = e.domain_of() else {
                    continue;
                };
                let categories = e.universe_categories();
                // must contain a decision variable
                if !categories.contains(&Category::Decision) {
                    continue;
                }
                // must not contain givens or quantified variables
                if categories.contains(&Category::Parameter)
                    || categories.contains(&Category::Quantified)
                {
                    continue;
                }
                let decl = symbols.gen_find_auxiliary(&domain);
                top_level_exprs.push(Expr::AuxDeclaration(
                    Metadata::new(),
                    Reference::new(decl.clone()),
                    Moo::new(e.clone()),
                ));
                *e = Expr::Atomic(Metadata::new(), Atom::Reference(Reference::new(decl)));
                child_changed = true;
3654
            }
        }
700
        (into_matrix_expr!(es;index_domain), child_changed)
358121
    });
142372
    if num_changed != 0 {
70
        Ok(RuleEffect::new(expr, top_level_exprs, symbols))
    } else {
142302
        Err(RuleNotApplicable)
    }
306768
}
/// Converts a Geq to an Ineq
///
/// ```text
/// x >= y ~> y <= x + 0 ~> ineq(y,x,0)
/// ```
#[register_rule("Minion", 4100, [Geq])]
21990
fn geq_to_ineq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
21990
    let Expr::Geq(meta, e1, e2) = expr.clone() else {
1253
        return Err(RuleNotApplicable);
    };
20737
    let Expr::Atomic(_, x) = Moo::unwrap_or_clone(e1) else {
        return Err(RuleNotApplicable);
    };
20737
    let Expr::Atomic(_, y) = Moo::unwrap_or_clone(e2) else {
        return Err(RuleNotApplicable);
    };
20737
    Ok(RuleEffect::pure(Expr::FlatIneq(
20737
        meta,
20737
        Moo::new(y),
20737
        Moo::new(x),
20737
        Box::new(Lit::Int(0)),
20737
    )))
21990
}
/// Converts a Leq to an Ineq
///
/// ```text
/// x <= y ~> x <= y + 0 ~> ineq(x,y,0)
/// ```
#[register_rule("Minion", 4100, [Leq])]
89463
fn leq_to_ineq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
89463
    let Expr::Leq(meta, e1, e2) = expr.clone() else {
1241
        return Err(RuleNotApplicable);
    };
88222
    let Expr::Atomic(_, x) = Moo::unwrap_or_clone(e1) else {
112
        return Err(RuleNotApplicable);
    };
88110
    let Expr::Atomic(_, y) = Moo::unwrap_or_clone(e2) else {
        return Err(RuleNotApplicable);
    };
88110
    Ok(RuleEffect::pure(Expr::FlatIneq(
88110
        meta,
88110
        Moo::new(x),
88110
        Moo::new(y),
88110
        Box::new(Lit::Int(0)),
88110
    )))
89463
}
// TODO: add this rule for geq
/// ```text
/// x <= y + k ~> ineq(x,y,k)
/// ```
#[register_rule("Minion", 4500, [Leq])]
263746
fn x_leq_y_plus_k_to_ineq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
263746
    let Expr::Leq(meta, e1, e2) = expr.clone() else {
2957
        return Err(RuleNotApplicable);
    };
260789
    let Expr::Atomic(_, x) = Moo::unwrap_or_clone(e1) else {
434
        return Err(RuleNotApplicable);
    };
260355
    let Expr::Sum(_, sum_exprs) = Moo::unwrap_or_clone(e2) else {
260344
        return Err(RuleNotApplicable);
    };
11
    let sum_exprs = (*sum_exprs).clone().into_list().ok_or(RuleNotApplicable)?;
7
    let (y, k) = match sum_exprs.as_slice() {
7
        [Expr::Atomic(_, y), Expr::Atomic(_, Atom::Literal(k))] => (y, k),
        [Expr::Atomic(_, Atom::Literal(k)), Expr::Atomic(_, y)] => (y, k),
        _ => {
            return Err(RuleNotApplicable);
        }
    };
7
    Ok(RuleEffect::pure(Expr::FlatIneq(
7
        meta,
7
        Moo::new(x),
7
        Moo::new(y.clone()),
7
        Box::new(k.clone()),
7
    )))
263746
}
/// ```text
/// y + k >= x ~> ineq(x,y,k)
/// ```
#[register_rule("Minion", 4800, [Geq])]
29304
fn y_plus_k_geq_x_to_ineq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
    // impl same as x_leq_y_plus_k but with lhs and rhs flipped
29304
    let Expr::Geq(meta, e2, e1) = expr.clone() else {
3470
        return Err(RuleNotApplicable);
    };
25834
    let Expr::Atomic(_, x) = Moo::unwrap_or_clone(e1) else {
756
        return Err(RuleNotApplicable);
    };
25078
    let Expr::Sum(_, sum_exprs) = Moo::unwrap_or_clone(e2) else {
21024
        return Err(RuleNotApplicable);
    };
4054
    let sum_exprs = Moo::unwrap_or_clone(sum_exprs)
4054
        .into_list()
4054
        .ok_or(RuleNotApplicable)?;
3907
    let (y, k) = match sum_exprs.as_slice() {
2737
        [Expr::Atomic(_, y), Expr::Atomic(_, Atom::Literal(k))] => (y, k),
7
        [Expr::Atomic(_, Atom::Literal(k)), Expr::Atomic(_, y)] => (y, k),
        _ => {
1163
            return Err(RuleNotApplicable);
        }
    };
2744
    Ok(RuleEffect::pure(Expr::FlatIneq(
2744
        meta,
2744
        Moo::new(x),
2744
        Moo::new(y.clone()),
2744
        Box::new(k.clone()),
2744
    )))
29304
}
/// Flattening rule for not(bool_lit)
///
/// For some boolean variable x:
/// ```text
///  not(x)      ~>  w-literal(x,0)
/// ```
///
/// ## Rationale
///
/// Minion's watched-and and watched-or constraints only takes other constraints as arguments.
///
/// This restates boolean variables as the equivalent constraint "SAT if x is true".
///
/// The regular bool_lit case is dealt with directly by the Minion solver interface (as it is a
/// trivial match).
#[register_rule("Minion", 4100, [Not])]
51936
fn not_literal_to_wliteral(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
51936
    match expr {
        Expr::Not(m, expr) => {
50683
            if let Expr::Atomic(_, Atom::Reference(reference)) = (**expr).clone()
50683
                && reference.ptr().domain().is_some_and(|d| d.is_bool())
            {
50683
                return Ok(RuleEffect::pure(Expr::FlatWatchedLiteral(
50683
                    m.clone(),
50683
                    reference,
50683
                    Lit::Bool(false),
50683
                )));
            }
            Err(RuleNotApplicable)
        }
1253
        _ => Err(RuleNotApplicable),
    }
51936
}
/// Flattening rule for not(X) in Minion, where X is a constraint.
///
/// ```text
/// not(X) ~> reify(X,0)
/// ```
///
/// This rule has lower priority than boolean_literal_to_wliteral so that we can assume that the
/// nested expressions are constraints not variables.
#[register_rule("Minion", 4090, [Not])]
947
fn not_constraint_to_reify(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
947
    if !matches!(expr, Expr::Not(_,c) if !matches!(**c, Expr::Atomic(_,_))) {
947
        return Err(RuleNotApplicable);
    }
    let Expr::Not(m, e) = expr else {
        unreachable!();
    };
    extra_check! {
        if !is_flat(e) {
            return Err(RuleNotApplicable);
        }
    };
    Ok(RuleEffect::pure(Expr::MinionReify(
        m.clone(),
        e.clone(),
        Atom::Literal(Lit::Bool(false)),
    )))
947
}
/// Converts an equality between a decision variable and a constant into `w-literal`.
///
/// ```text
/// x = k  ~>  w-literal(x, k)
/// k = x  ~>  w-literal(x, k)
/// ```
///
/// SavileRow posts forced assignments this way. Minion's `eq(x,k)` is valid, but
/// `w-literal` is the watched form used for domain assignments and propagates
/// more cleanly alongside other watched constraints.
#[register_rule("Minion", 4400, [Eq])]
152286
fn eq_atom_literal_to_wliteral(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
152286
    let Expr::Eq(m, a, b) = expr else {
2741
        return Err(RuleNotApplicable);
    };
149545
    let (reference, lit) = match (a.as_ref(), b.as_ref()) {
        (Expr::Atomic(_, Atom::Reference(reference)), Expr::Atomic(_, Atom::Literal(lit))) => {
100491
            (reference, lit)
        }
        (Expr::Atomic(_, Atom::Literal(lit)), Expr::Atomic(_, Atom::Reference(reference))) => {
1582
            (reference, lit)
        }
47472
        _ => return Err(RuleNotApplicable),
    };
102073
    Ok(RuleEffect::pure(Expr::FlatWatchedLiteral(
102073
        m.clone(),
102073
        reference.clone(),
102073
        lit.clone(),
102073
    )))
152286
}
/// Converts an equality to a boolean constraint into a `reify` constraint.
///
/// ```text
/// x =aux c ~> reify(c,x)
/// x = c ~> reify(c,x)
///
/// where c is a non-atomic boolean constraint
/// ```
///
/// Atomic equalities such as `x = false` or `x = y` are left alone: Minion can emit `eq`
/// directly, and turning `x = false` into `reify(false, x)` embeds a posted constraint form
/// as a Boolean subexpression (for example as the LHS of `Imply`), which neither
/// [`flatten_imply`] nor [`introduce_reifyimply_ineq_from_imply`] can lower.
#[register_rule("Minion", 4400, [AuxDeclaration, Eq])]
286948
fn bool_eq_to_reify(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
286948
    let (atom, e): (Atom, Moo<Expr>) = match expr {
134744
        Expr::AuxDeclaration(_, reference, e) => Ok((Atom::from(reference.clone()), e.clone())),
149547
        Expr::Eq(_, a, b) => match (a.as_ref(), b.as_ref()) {
106694
            (Expr::Atomic(_, atom), _) => Ok((atom.clone(), b.clone())),
40307
            (_, Expr::Atomic(_, atom)) => Ok((atom.clone(), a.clone())),
2546
            _ => Err(RuleNotApplicable),
        },
2657
        _ => Err(RuleNotApplicable),
5203
    }?;
    // Reify targets a constraint, not another atom. Atomic Bool equalities stay as `eq`.
281745
    if matches!(e.as_ref(), Expr::Atomic(_, _)) {
106236
        return Err(RuleNotApplicable);
175509
    }
    // e does not have to be valid minion constraint yet, as long as we know it can turn into one
    // (i.e. it is boolean).
175509
    if e.as_ref().return_type() != ReturnType::Bool {
117123
        return Err(RuleNotApplicable);
58386
    };
58386
    Ok(RuleEffect::pure(Expr::MinionReify(
58386
        Metadata::new(),
58386
        e,
58386
        atom,
58386
    )))
286948
}
/// Converts an iff into Minion form in one step.
///
/// ```text
/// Iff(x, c) ~> reify(c, x)     when x is atomic and c is a non-atomic Bool
/// Iff(a, b) ~> Eq(a, b)        otherwise (including atomic Bool equalities)
/// ```
///
/// Going via `Iff ~> Eq ~> reify` used two successful rewrites (and two dirty
/// cascades) for every ship-linkage constraint on models like
/// solitaire_battleship. Fusing the common case removes that intermediate pass.
#[register_rule("Minion", 4400, [Iff])]
4092
fn iff_to_eq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
4092
    let Expr::Iff(_, x, y) = expr else {
2807
        return Err(RuleNotApplicable);
    };
1285
    match (x.as_ref(), y.as_ref()) {
700
        (Expr::Atomic(_, atom), other) if !matches!(other, Expr::Atomic(_, _)) => {
665
            if other.return_type() != ReturnType::Bool {
                return Err(RuleNotApplicable);
665
            }
665
            Ok(RuleEffect::pure(Expr::MinionReify(
665
                Metadata::new(),
665
                y.clone(),
665
                atom.clone(),
665
            )))
        }
175
        (other, Expr::Atomic(_, atom)) if !matches!(other, Expr::Atomic(_, _)) => {
140
            if other.return_type() != ReturnType::Bool {
                return Err(RuleNotApplicable);
140
            }
140
            Ok(RuleEffect::pure(Expr::MinionReify(
140
                Metadata::new(),
140
                x.clone(),
140
                atom.clone(),
140
            )))
        }
480
        _ => Ok(RuleEffect::pure(Expr::Eq(
480
            Metadata::new(),
480
            x.clone(),
480
            y.clone(),
480
        ))),
    }
4092
}
#[cfg(test)]
mod tests {
    use super::*;
    use conjure_cp::ast::{DeclarationPtr, Domain};
    use conjure_cp::matrix_expr;
    use conjure_cp::rule_engine::{ApplicationError, get_rule_by_name};
    /// Builds a boolean decision-variable atomic expression.
10
    fn bool_atom(name: &str) -> Expr {
10
        Expr::Atomic(
10
            Metadata::new(),
10
            Atom::Reference(Reference::new(DeclarationPtr::new_find(
10
                Name::user(name),
10
                Domain::bool(),
10
            ))),
10
        )
10
    }
    /// Builds an integer decision-variable atomic expression.
10
    fn int_atom(name: &str) -> Expr {
10
        Expr::Atomic(
10
            Metadata::new(),
10
            Atom::Reference(Reference::new(DeclarationPtr::new_find(
10
                Name::user(name),
10
                Domain::int(vec![Range::Bounded(1, 5)]),
10
            ))),
10
        )
10
    }
    /// Atomic Boolean literal.
2
    fn bool_lit(value: bool) -> Expr {
2
        Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Bool(value)))
2
    }
    #[test]
1
    fn introduce_lee_distance_atom_lowers_min_abs_complement() {
1
        let a = int_atom("a");
1
        let b = int_atom("b");
1
        let diff = Expr::Minus(Metadata::new(), Moo::new(a), Moo::new(b));
1
        let abs_arm = Expr::Abs(Metadata::new(), Moo::new(diff.clone()));
1
        let complement = Expr::Minus(
1
            Metadata::new(),
1
            Moo::new(Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Int(4)))),
1
            Moo::new(Expr::Abs(Metadata::new(), Moo::new(diff))),
1
        );
1
        let expr = Expr::Min(Metadata::new(), Moo::new(matrix_expr![abs_arm, complement]));
1
        let rule = get_rule_by_name("introduce_lee_distance_atom").expect("rule registered");
1
        let result = rule
1
            .apply(&expr, &SymbolTable::new())
1
            .expect("Lee atom should lower in one step");
1
        assert!(
1
            matches!(result.new_expression, Expr::Atomic(_, _)),
            "Lee min must become an auxiliary atom"
        );
1
        assert!(
1
            result
1
                .new_top
1
                .iter()
3
                .any(|c| matches!(c, Expr::FlatAbsEq(_, _, _))),
            "must post FlatAbsEq for the shared absolute value"
        );
1
        assert!(
1
            result
1
                .new_top
1
                .iter()
6
                .any(|c| matches!(c, Expr::FlatMinEq(_, vars, _) if vars.len() == 2)),
            "must post FlatMinEq over abs and complement"
        );
1
        assert!(
1
            result
1
                .new_top
1
                .iter()
6
                .filter(|c| matches!(c, Expr::FlatSumLeq(_, vars, _) if vars.len() == 2))
                .count()
1
                == 1
1
                && result
1
                    .new_top
1
                    .iter()
6
                    .filter(|c| matches!(c, Expr::FlatSumGeq(_, vars, _) if vars.len() == 2))
                    .count()
1
                    == 1,
            "must channel abs + complement = alphabet size"
        );
1
        assert!(
1
            result.new_top.iter().any(|c| {
1
                matches!(
1
                    c,
                    Expr::FlatWeightedSumLeq(_, coeffs, vars, _)
                        | Expr::FlatWeightedSumGeq(_, coeffs, vars, _)
1
                        if coeffs.len() == 2 && vars.len() == 2
                )
1
            }),
            "atomic minus should channel through a weighted sum"
        );
1
    }
    #[test]
1
    fn introduce_lee_distance_atom_accepts_sum_complement_form() {
1
        let a = int_atom("a");
1
        let b = int_atom("b");
1
        let diff = Expr::Minus(Metadata::new(), Moo::new(a), Moo::new(b));
1
        let abs_arm = Expr::Abs(Metadata::new(), Moo::new(diff.clone()));
1
        let complement = Expr::Sum(
1
            Metadata::new(),
1
            Moo::new(matrix_expr![
1
                Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Int(4))),
1
                Expr::Neg(
1
                    Metadata::new(),
1
                    Moo::new(Expr::Abs(Metadata::new(), Moo::new(diff))),
1
                ),
1
            ]),
1
        );
1
        let expr = Expr::Min(Metadata::new(), Moo::new(matrix_expr![abs_arm, complement]));
1
        let rule = get_rule_by_name("introduce_lee_distance_atom").expect("rule registered");
1
        rule.apply(&expr, &SymbolTable::new())
1
            .expect("sum-form Lee complement should also match");
1
    }
    #[test]
1
    fn introduce_lee_distance_atom_rejects_unrelated_min() {
1
        let expr = Expr::Min(
1
            Metadata::new(),
1
            Moo::new(matrix_expr![int_atom("a"), int_atom("b")]),
1
        );
1
        let rule = get_rule_by_name("introduce_lee_distance_atom").expect("rule registered");
1
        let err = rule.apply(&expr, &SymbolTable::new()).unwrap_err();
1
        assert!(matches!(err, ApplicationError::RuleNotApplicable));
1
    }
    #[test]
1
    fn introduce_mineq_from_min_emits_flat_min_eq() {
1
        let expr = Expr::Min(
1
            Metadata::new(),
1
            Moo::new(matrix_expr![int_atom("a"), int_atom("b")]),
1
        );
1
        let rule = get_rule_by_name("introduce_mineq_from_min").expect("rule registered");
1
        let result = rule
1
            .apply(&expr, &SymbolTable::new())
1
            .expect("min of atoms should lower to FlatMinEq");
1
        assert!(
1
            matches!(result.new_expression, Expr::Atomic(_, _)),
            "bare min must become an auxiliary atom"
        );
1
        assert!(
1
            result
1
                .new_top
1
                .iter()
1
                .any(|c| matches!(c, Expr::FlatMinEq(_, vars, _) if vars.len() == 2)),
            "must post FlatMinEq over the two atoms"
        );
1
    }
    #[test]
1
    fn introduce_mineq_from_aux_declaration() {
        let result_decl =
1
            DeclarationPtr::new_find(Name::user("r"), Domain::int(vec![Range::Bounded(1, 5)]));
1
        let expr = Expr::AuxDeclaration(
1
            Metadata::new(),
1
            Reference::new(result_decl.clone()),
1
            Moo::new(Expr::Min(
1
                Metadata::new(),
1
                Moo::new(matrix_expr![int_atom("a"), int_atom("b")]),
1
            )),
1
        );
1
        let rule = get_rule_by_name("introduce_mineq").expect("rule registered");
1
        let result = rule
1
            .apply(&expr, &SymbolTable::new())
1
            .expect("aux = min([...]) should become FlatMinEq");
1
        assert!(
1
            matches!(result.new_expression, Expr::FlatMinEq(_, vars, _) if vars.len() == 2),
            "auxiliary min must lower to FlatMinEq"
        );
1
    }
    #[test]
1
    fn introduce_weighted_sum_uses_bool_atom_without_to_int_aux() {
1
        let bool_var = bool_atom("b");
1
        let to_int = Expr::ToInt(Metadata::new(), Moo::new(bool_var.clone()));
1
        let expr = Expr::Eq(
1
            Metadata::new(),
1
            Moo::new(Expr::Sum(Metadata::new(), Moo::new(matrix_expr![to_int]))),
1
            Moo::new(Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Int(1)))),
1
        );
1
        let rule = get_rule_by_name("introduce_weighted_sumleq_sumgeq").expect("rule registered");
1
        let result = rule
1
            .apply(&expr, &SymbolTable::new())
1
            .expect("sum of toInt(bool) should lower");
1
        assert!(
1
            result.new_top.is_empty(),
            "toInt(bool atom) must not create aux vars; got {:?}",
            result.new_top
        );
1
        match &result.new_expression {
            Expr::And(_, matrix) => {
1
                let list = matrix.as_ref().clone().unwrap_list().expect("and list");
1
                assert!(
1
                    list.iter().any(|c| matches!(
1
                        c,
                        Expr::FlatSumLeq(_, vars, _) | Expr::FlatSumGeq(_, vars, _)
                        if vars.len() == 1
                    )),
                    "expected FlatSum over the bool atom, got {:?}",
                    result.new_expression
                );
            }
            other => panic!("expected and of sumleq/sumgeq, got {other:?}"),
        }
1
    }
    #[test]
1
    fn introduce_weighted_sum_lowers_product_of_to_int_bools() {
        // bibd/opd: sum([product([toInt(a), toInt(b)]), ...]) <= k
        // Stripping toInt(bool) must not prevent FlatProductEq aux domain inference.
1
        let a = Expr::ToInt(Metadata::new(), Moo::new(bool_atom("a")));
1
        let b = Expr::ToInt(Metadata::new(), Moo::new(bool_atom("b")));
1
        let product = Expr::Product(Metadata::new(), Moo::new(matrix_expr![a, b]));
1
        let expr = Expr::Leq(
1
            Metadata::new(),
1
            Moo::new(Expr::Sum(Metadata::new(), Moo::new(matrix_expr![product]))),
1
            Moo::new(Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Int(1)))),
1
        );
1
        let rule = get_rule_by_name("introduce_weighted_sumleq_sumgeq").expect("rule registered");
1
        let result = rule
1
            .apply(&expr, &SymbolTable::new())
1
            .expect("sum of product(toInt(bool), toInt(bool)) should lower");
1
        assert!(
1
            matches!(result.new_expression, Expr::FlatSumLeq(_, ref vars, _) if vars.len() == 1),
            "expected FlatSumLeq over product aux, got {:?}",
            result.new_expression
        );
1
        assert!(
1
            result
1
                .new_top
1
                .iter()
1
                .any(|c| matches!(c, Expr::FlatProductEq(_, _, _, _))),
            "must post FlatProductEq over the bool atoms; got {:?}",
            result.new_top
        );
1
        assert!(
1
            !result
1
                .new_top
1
                .iter()
1
                .any(|c| matches!(c, Expr::AuxDeclaration(_, _, e) if matches!(e.as_ref(), Expr::ToInt(_, _)))),
            "toInt(bool) factors must not create toInt aux vars; got {:?}",
            result.new_top
        );
1
    }
    #[test]
1
    fn eq_atom_literal_to_wliteral_lowers_forced_assignment() {
1
        let expr = Expr::Eq(
1
            Metadata::new(),
1
            Moo::new(bool_atom("x")),
1
            Moo::new(bool_lit(false)),
1
        );
1
        let rule = get_rule_by_name("eq_atom_literal_to_wliteral").expect("rule registered");
1
        let result = rule
1
            .apply(&expr, &SymbolTable::new())
1
            .expect("x = false should become w-literal");
1
        assert!(
1
            matches!(
1
                result.new_expression,
                Expr::FlatWatchedLiteral(_, _, Lit::Bool(false))
            ),
            "expected FlatWatchedLiteral(x, false), got {:?}",
            result.new_expression
        );
1
    }
    #[test]
1
    fn bool_eq_to_reify_refuses_atomic_false_equality() {
1
        let expr = Expr::Eq(
1
            Metadata::new(),
1
            Moo::new(bool_atom("x")),
1
            Moo::new(bool_lit(false)),
1
        );
1
        let rule = get_rule_by_name("bool_eq_to_reify").expect("rule registered");
1
        let err = rule.apply(&expr, &SymbolTable::new()).unwrap_err();
1
        assert!(
1
            matches!(err, ApplicationError::RuleNotApplicable),
            "x = false must not become reify(false, x)"
        );
1
    }
    #[test]
1
    fn bool_eq_to_reify_refuses_atomic_variable_equality() {
1
        let expr = Expr::Eq(
1
            Metadata::new(),
1
            Moo::new(bool_atom("x")),
1
            Moo::new(bool_atom("y")),
1
        );
1
        let rule = get_rule_by_name("bool_eq_to_reify").expect("rule registered");
1
        let err = rule.apply(&expr, &SymbolTable::new()).unwrap_err();
1
        assert!(
1
            matches!(err, ApplicationError::RuleNotApplicable),
            "x = y must stay as eq, not reify"
        );
1
    }
    #[test]
1
    fn bool_eq_to_reify_still_applies_to_constraint_equality() {
1
        let constraint = Expr::And(
1
            Metadata::new(),
1
            Moo::new(matrix_expr![bool_atom("a"), bool_atom("b")]),
1
        );
1
        let expr = Expr::Eq(
1
            Metadata::new(),
1
            Moo::new(bool_atom("r")),
1
            Moo::new(constraint),
1
        );
1
        let rule = get_rule_by_name("bool_eq_to_reify").expect("rule registered");
1
        let result = rule
1
            .apply(&expr, &SymbolTable::new())
1
            .expect("r = and(a,b) should reify");
1
        assert!(
1
            matches!(result.new_expression, Expr::MinionReify(_, _, _)),
            "non-atomic Boolean equality must still become MinionReify"
        );
1
    }
    /// Builds a tuple literal expression `(a, b)`.
5
    fn tuple_lit(a: i32, b: bool) -> Expr {
5
        Expr::Atomic(
5
            Metadata::new(),
5
            Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Tuple(vec![
5
                Lit::Int(a),
5
                Lit::Bool(b),
5
            ]))),
5
        )
5
    }
    /// Builds `[e1,...,en; int(1..n)]` as an `Expr`, matching the shape
    /// `FunctionAsRelation`/`FunctionExplicit`'s own `image()` rules build for
    /// `domain_values_matrix`.
2
    fn one_based_matrix(elems: Vec<Expr>) -> Expr {
2
        let n = elems.len() as i32;
2
        into_matrix_expr![elems; Domain::int(vec![Range::Bounded(1, n)])]
2
    }
    #[test]
1
    fn fold_constant_index_of_to_index_finds_a_compound_literal() {
        // Regression: indexOf([(7,false),(7,true),(8,false),(8,true)], (8,true)) used to only
        // resolve for a plain int search value (`expr_int_literal` failing outright for a
        // compound one), leaving the IndexOf node -- and eventually the raw tuple literal --
        // unresolved all the way to Minion.
1
        let matrix = one_based_matrix(vec![
1
            tuple_lit(7, false),
1
            tuple_lit(7, true),
1
            tuple_lit(8, false),
1
            tuple_lit(8, true),
1
        ]);
1
        let index_of = Expr::IndexOf(
1
            Metadata::new(),
1
            Moo::new(matrix),
1
            Moo::new(tuple_lit(8, true)),
1
        );
1
        let folded = fold_constant_index_of_to_index(&index_of).expect("should fold");
1
        assert_eq!(folded, Expr::from(4));
1
    }
    #[test]
1
    fn fold_constant_index_of_to_index_still_finds_a_plain_int() {
        // The pre-existing scalar case must keep working once the lookup is generalised.
1
        let matrix = one_based_matrix(vec![Expr::from(10), Expr::from(20), Expr::from(30)]);
1
        let index_of = Expr::IndexOf(Metadata::new(), Moo::new(matrix), Moo::new(Expr::from(20)));
1
        let folded = fold_constant_index_of_to_index(&index_of).expect("should fold");
1
        assert_eq!(folded, Expr::from(2));
1
    }
}