conjure_core/rules/
minion.rs

1/************************************************************************/
2/*        Rules for translating to Minion-supported constraints         */
3/************************************************************************/
4
5use std::convert::TryInto;
6use std::rc::Rc;
7
8use crate::ast::Declaration;
9use crate::ast::{Atom, Domain, Expression as Expr, Literal as Lit, ReturnType, SymbolTable};
10
11use crate::matrix_expr;
12use crate::metadata::Metadata;
13use crate::rule_engine::{
14    register_rule, register_rule_set, ApplicationError, ApplicationResult, Reduction,
15};
16use crate::rules::extra_check;
17
18use crate::solver::SolverFamily;
19use itertools::Itertools;
20use uniplate::Uniplate;
21use ApplicationError::*;
22
23use super::utils::{is_flat, to_aux_var};
24
25register_rule_set!("Minion", ("Base"), (SolverFamily::Minion));
26
27#[register_rule(("Minion", 4200))]
28fn introduce_producteq(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
29    // product = val
30    let val: Atom;
31    let product: Expr;
32
33    match expr.clone() {
34        Expr::Eq(_m, a, b) => {
35            let a_atom: Option<&Atom> = (&a).try_into().ok();
36            let b_atom: Option<&Atom> = (&b).try_into().ok();
37
38            if let Some(f) = a_atom {
39                // val = product
40                val = f.clone();
41                product = *b;
42            } else if let Some(f) = b_atom {
43                // product = val
44                val = f.clone();
45                product = *a;
46            } else {
47                return Err(RuleNotApplicable);
48            }
49        }
50        Expr::AuxDeclaration(_m, name, e) => {
51            val = name.into();
52            product = *e;
53        }
54        _ => {
55            return Err(RuleNotApplicable);
56        }
57    }
58
59    if !(matches!(product, Expr::Product(_, _,))) {
60        return Err(RuleNotApplicable);
61    }
62
63    let Expr::Product(_, mut factors) = product else {
64        return Err(RuleNotApplicable);
65    };
66
67    if factors.len() < 2 {
68        return Err(RuleNotApplicable);
69    }
70
71    // Product is a vecop, but FlatProductEq a binop.
72    // Introduce auxvars until it is a binop
73
74    // the expression returned will be x*y=val.
75    // if factors is > 2 arguments, y will be an auxiliary variable
76
77    #[allow(clippy::unwrap_used)] // should never panic - length is checked above
78    let x: Atom = factors
79        .pop()
80        .unwrap()
81        .try_into()
82        .or(Err(RuleNotApplicable))?;
83
84    #[allow(clippy::unwrap_used)] // should never panic - length is checked above
85    let mut y: Atom = factors
86        .pop()
87        .unwrap()
88        .try_into()
89        .or(Err(RuleNotApplicable))?;
90
91    let mut symbols = symbols.clone();
92    let mut new_tops: Vec<Expr> = vec![];
93
94    // FIXME: add a test for this
95    while let Some(next_factor) = factors.pop() {
96        // Despite adding auxvars, I still require all atoms as factors, making this rule act
97        // similar to other introduction rules.
98        let next_factor_atom: Atom = next_factor.clone().try_into().or(Err(RuleNotApplicable))?;
99
100        let aux_var = symbols.gensym();
101        // TODO: find this domain without having to make unnecessary Expr and Metadata objects
102        // Just using the domain of expr doesn't work
103        let aux_domain = Expr::Product(Metadata::new(), vec![y.clone().into(), next_factor])
104            .domain_of(&symbols)
105            .ok_or(ApplicationError::DomainError)?;
106
107        symbols.insert(Rc::new(Declaration::new_var(aux_var.clone(), aux_domain)));
108
109        let new_top_expr =
110            Expr::FlatProductEq(Metadata::new(), y, next_factor_atom, aux_var.clone().into());
111
112        new_tops.push(new_top_expr);
113        y = aux_var.into();
114    }
115
116    Ok(Reduction::new(
117        Expr::FlatProductEq(Metadata::new(), x, y, val),
118        new_tops,
119        symbols,
120    ))
121}
122
123/// Introduces `FlatWeightedSumLeq`, `FlatWeightedSumGeq`, `FlatSumLeq`, FlatSumGeq` constraints.
124///
125/// If the input is a weighted sum, the weighted sum constraints are used, otherwise the standard
126/// sum constraints are used.
127///
128/// # Details
129/// This rule is a bit unusual compared to other introduction rules in that
130/// it does its own flattening.
131///
132/// Weighted sums are expressed as sums of products, which are not
133/// flat. Flattening a weighted sum generically makes it indistinguishable
134/// from a sum:
135///
136///```text
137/// 1*a + 2*b + 3*c + d <= 10
138///   ~~> flatten_vecop
139/// __0 + __1 + __2 + d <= 10
140///
141/// with new top level constraints
142///
143/// __0 =aux 1*a
144/// __1 =aux 2*b
145/// __2 =aux 3*c
146/// ```
147///
148/// Therefore, introduce_weightedsumleq_sumgeq does its own flattening.
149///
150/// Having custom flattening semantics means that we can make more things
151/// weighted sums.
152///
153/// For example, consider `a + 2*b + 3*c*d + (e / f) + 5*(g/h) <= 18`. This
154/// rule turns this into a single flat_weightedsumleq constraint:
155///
156///```text
157/// a + 2*b + 3*c*d + (e/f) + 5*(g/h) <= 30
158///
159///   ~> introduce_weightedsumleq_sumgeq
160///
161/// flat_weightedsumleq([1,2,3,1,5],[a,b,__0,__1,__2],30)
162///
163/// with new top level constraints
164///
165/// __0 = c*d
166/// __1 = e/f
167/// __2 = g/h
168/// ```
169///
170/// The rules to turn terms into coefficient variable pairs are the following:
171///
172/// 1. Non-weighted atom: `a ~> (1,a)`
173/// 2. Other non-weighted term: `e ~> (1,__0)`, with new constraint `__0 =aux e`
174/// 3. Weighted atom: `c*a ~> (c,a)`
175/// 4. Weighted non-atom: `c*e ~> (c,__0)` with new constraint` __0 =aux e`
176/// 5. Weighted product: `c*e*f ~> (c,__0)` with new constraint `__0 =aux (e*f)`
177/// 6. Negated atom: `-x ~> (-1,x)`
178/// 7. Negated expression: `-e ~> (-1,__0)` with new constraint `__0 = e`
179///
180/// Cases 6 and 7 could potentially be a normalising rule `-e ~> -1*e`. However, I think that we
181/// should only turn negations into a product when they are inside a sum, not all the time.
182#[register_rule(("Minion", 4600))]
183fn introduce_weighted_sumleq_sumgeq(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
184    // Keep track of which type of (in)equality was in the input, and use this to decide what
185    // constraints to make at the end
186
187    // We handle Eq directly in this rule instead of letting it be decomposed to <= and >=
188    // elsewhere, as this caused cyclic rule application:
189    //
190    // ```
191    // 2*a + b = c
192    //
193    //   ~~> sumeq_to_inequalities
194    //
195    // 2*a + b <=c /\ 2*a + b >= c
196    //
197    // --
198    //
199    // 2*a + b <= c
200    //
201    //   ~~> flatten_generic
202    // __1 <=c
203    //
204    // with new top level constraint
205    //
206    // 2*a + b =aux __1
207    //
208    // --
209    //
210    // 2*a + b =aux __1
211    //
212    //   ~~> sumeq_to_inequalities
213    //
214    // LOOP!
215    // ```
216    enum EqualityKind {
217        Eq,
218        Leq,
219        Geq,
220    }
221
222    // Given the LHS, RHS, and the type of inequality, return the sum, total, and new inequality.
223    //
224    // The inequality returned is the one that puts the sum is on the left hand side and the total
225    // on the right hand side.
226    //
227    // For example, `1 <= a + b` will result in ([a,b],1,Geq).
228    fn match_sum_total(
229        a: Expr,
230        b: Expr,
231        equality_kind: EqualityKind,
232    ) -> Result<(Vec<Expr>, Atom, EqualityKind), ApplicationError> {
233        match (a, b, equality_kind) {
234            (Expr::Sum(_, sum_terms), Expr::Atomic(_, total), EqualityKind::Leq) => {
235                let sum_terms = sum_terms.unwrap_list().ok_or(RuleNotApplicable)?;
236                Ok((sum_terms, total, EqualityKind::Leq))
237            }
238            (Expr::Atomic(_, total), Expr::Sum(_, sum_terms), EqualityKind::Leq) => {
239                let sum_terms = sum_terms.unwrap_list().ok_or(RuleNotApplicable)?;
240                Ok((sum_terms, total, EqualityKind::Geq))
241            }
242            (Expr::Sum(_, sum_terms), Expr::Atomic(_, total), EqualityKind::Geq) => {
243                let sum_terms = sum_terms.unwrap_list().ok_or(RuleNotApplicable)?;
244                Ok((sum_terms, total, EqualityKind::Geq))
245            }
246            (Expr::Atomic(_, total), Expr::Sum(_, sum_terms), EqualityKind::Geq) => {
247                let sum_terms = sum_terms.unwrap_list().ok_or(RuleNotApplicable)?;
248                Ok((sum_terms, total, EqualityKind::Leq))
249            }
250            (Expr::Sum(_, sum_terms), Expr::Atomic(_, total), EqualityKind::Eq) => {
251                let sum_terms = sum_terms.unwrap_list().ok_or(RuleNotApplicable)?;
252                Ok((sum_terms, total, EqualityKind::Eq))
253            }
254            (Expr::Atomic(_, total), Expr::Sum(_, sum_terms), EqualityKind::Eq) => {
255                let sum_terms = sum_terms.unwrap_list().ok_or(RuleNotApplicable)?;
256                Ok((sum_terms, total, EqualityKind::Eq))
257            }
258            _ => Err(RuleNotApplicable),
259        }
260    }
261
262    let (sum_exprs, total, equality_kind) = match expr.clone() {
263        Expr::Leq(_, a, b) => Ok(match_sum_total(*a, *b, EqualityKind::Leq)?),
264        Expr::Geq(_, a, b) => Ok(match_sum_total(*a, *b, EqualityKind::Geq)?),
265        Expr::Eq(_, a, b) => Ok(match_sum_total(*a, *b, EqualityKind::Eq)?),
266        Expr::AuxDeclaration(_, n, a) => {
267            let total: Atom = n.into();
268            if let Expr::Sum(_, sum_terms) = *a {
269                let sum_terms = sum_terms.unwrap_list().ok_or(RuleNotApplicable)?;
270                Ok((sum_terms, total, EqualityKind::Eq))
271            } else {
272                Err(RuleNotApplicable)
273            }
274        }
275        _ => Err(RuleNotApplicable),
276    }?;
277
278    let mut new_top_exprs: Vec<Expr> = vec![];
279    let mut symbols = symbols.clone();
280
281    let mut coefficients: Vec<Lit> = vec![];
282    let mut vars: Vec<Atom> = vec![];
283
284    // if all coefficients are 1, use normal sum rule instead
285    let mut found_non_one_coeff = false;
286
287    // for each sub-term, get the coefficient and the variable, flattening if necessary.
288    for expr in sum_exprs {
289        let (coeff, var): (Lit, Atom) = match expr {
290            // atom: v ~> 1*v
291            Expr::Atomic(_, atom) => (Lit::Int(1), atom),
292
293            // assuming normalisation / partial eval, literal will be first term
294
295            // weighted sum term: c * e.
296            // e can either be an atom, the rest of the product to be flattened, or an other expression that needs
297            // flattening
298            Expr::Product(_, factors)
299                if factors.len() > 1 && matches!(factors[0], Expr::Atomic(_, Atom::Literal(_))) =>
300            {
301                match &factors[..] {
302                    // c * <atom>
303                    [Expr::Atomic(_, Atom::Literal(c)), Expr::Atomic(_, atom)] => {
304                        (c.clone(), atom.clone())
305                    }
306
307                    // c * <some non-flat expression>
308                    [Expr::Atomic(_, Atom::Literal(c)), e1] => {
309                        #[allow(clippy::unwrap_used)] // aux var failing is a bug
310                        let aux_var_info = to_aux_var(e1, &symbols).unwrap();
311
312                        symbols = aux_var_info.symbols();
313                        let var = aux_var_info.as_atom();
314                        new_top_exprs.push(aux_var_info.top_level_expr());
315                        (c.clone(), var)
316                    }
317
318                    // c * a * b * c * ...
319                    [Expr::Atomic(_, Atom::Literal(c)), ref rest @ ..] => {
320                        let e1 = Expr::Product(Metadata::new(), rest.to_vec());
321
322                        #[allow(clippy::unwrap_used)] // aux var failing is a bug
323                        let aux_var_info = to_aux_var(&e1, &symbols).unwrap();
324
325                        symbols = aux_var_info.symbols();
326                        let var = aux_var_info.as_atom();
327                        new_top_exprs.push(aux_var_info.top_level_expr());
328                        (c.clone(), var)
329                    }
330
331                    _ => unreachable!(),
332                }
333            }
334
335            // negated terms: `-e ~> -1*e`
336            //
337            // flatten e if non-atomic
338            Expr::Neg(_, e) => {
339                // needs flattening
340                let v: Atom = if let Some(aux_var_info) = to_aux_var(&e, &symbols) {
341                    symbols = aux_var_info.symbols();
342                    new_top_exprs.push(aux_var_info.top_level_expr());
343                    aux_var_info.as_atom()
344                } else {
345                    // if we can't flatten it, it must be an atom!
346                    #[allow(clippy::unwrap_used)]
347                    e.try_into().unwrap()
348                };
349
350                (Lit::Int(-1), v)
351            }
352
353            // flatten non-flat terms without coefficients: e1 ~> (1,__0)
354            //
355            // includes products without coefficients.
356            e => {
357                //
358                let aux_var_info = to_aux_var(&e, &symbols).ok_or(RuleNotApplicable)?;
359
360                symbols = aux_var_info.symbols();
361                let var = aux_var_info.as_atom();
362                new_top_exprs.push(aux_var_info.top_level_expr());
363                (Lit::Int(1), var)
364            }
365        };
366
367        let coeff_num: i32 = coeff.clone().try_into().or(Err(RuleNotApplicable))?;
368        found_non_one_coeff |= coeff_num != 1;
369        coefficients.push(coeff);
370        vars.push(var);
371    }
372
373    let use_weighted_sum = found_non_one_coeff;
374    // the expr should use a regular sum instead if the coefficients are all 1.
375    let new_expr: Expr = match (equality_kind, use_weighted_sum) {
376        (EqualityKind::Eq, true) => Expr::And(
377            Metadata::new(),
378            Box::new(matrix_expr![
379                Expr::FlatWeightedSumLeq(
380                    Metadata::new(),
381                    coefficients.clone(),
382                    vars.clone(),
383                    total.clone(),
384                ),
385                Expr::FlatWeightedSumGeq(Metadata::new(), coefficients, vars, total),
386            ]),
387        ),
388        (EqualityKind::Eq, false) => Expr::And(
389            Metadata::new(),
390            Box::new(matrix_expr![
391                Expr::FlatSumLeq(Metadata::new(), vars.clone(), total.clone()),
392                Expr::FlatSumGeq(Metadata::new(), vars, total),
393            ]),
394        ),
395        (EqualityKind::Leq, true) => {
396            Expr::FlatWeightedSumLeq(Metadata::new(), coefficients, vars, total)
397        }
398        (EqualityKind::Leq, false) => Expr::FlatSumLeq(Metadata::new(), vars, total),
399        (EqualityKind::Geq, true) => {
400            Expr::FlatWeightedSumGeq(Metadata::new(), coefficients, vars, total)
401        }
402        (EqualityKind::Geq, false) => Expr::FlatSumGeq(Metadata::new(), vars, total),
403    };
404
405    Ok(Reduction::new(new_expr, new_top_exprs, symbols))
406}
407
408#[register_rule(("Minion", 4200))]
409fn introduce_diveq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
410    // div = val
411    let val: Atom;
412    let div: Expr;
413    let meta: Metadata;
414
415    match expr.clone() {
416        Expr::Eq(m, a, b) => {
417            meta = m;
418
419            let a_atom: Option<&Atom> = (&a).try_into().ok();
420            let b_atom: Option<&Atom> = (&b).try_into().ok();
421
422            if let Some(f) = a_atom {
423                // val = div
424                val = f.clone();
425                div = *b;
426            } else if let Some(f) = b_atom {
427                // div = val
428                val = f.clone();
429                div = *a;
430            } else {
431                return Err(RuleNotApplicable);
432            }
433        }
434        Expr::AuxDeclaration(m, name, e) => {
435            meta = m;
436            val = name.into();
437            div = *e;
438        }
439        _ => {
440            return Err(RuleNotApplicable);
441        }
442    }
443
444    if !(matches!(div, Expr::SafeDiv(_, _, _))) {
445        return Err(RuleNotApplicable);
446    }
447
448    let children = div.children();
449    let a: &Atom = (&children[0]).try_into().or(Err(RuleNotApplicable))?;
450    let b: &Atom = (&children[1]).try_into().or(Err(RuleNotApplicable))?;
451
452    Ok(Reduction::pure(Expr::MinionDivEqUndefZero(
453        meta.clone_dirty(),
454        a.clone(),
455        b.clone(),
456        val,
457    )))
458}
459
460#[register_rule(("Minion", 4200))]
461fn introduce_modeq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
462    // div = val
463    let val: Atom;
464    let div: Expr;
465    let meta: Metadata;
466
467    match expr.clone() {
468        Expr::Eq(m, a, b) => {
469            meta = m;
470            let a_atom: Option<&Atom> = (&a).try_into().ok();
471            let b_atom: Option<&Atom> = (&b).try_into().ok();
472
473            if let Some(f) = a_atom {
474                // val = div
475                val = f.clone();
476                div = *b;
477            } else if let Some(f) = b_atom {
478                // div = val
479                val = f.clone();
480                div = *a;
481            } else {
482                return Err(RuleNotApplicable);
483            }
484        }
485        Expr::AuxDeclaration(m, name, e) => {
486            meta = m;
487            val = name.into();
488            div = *e;
489        }
490        _ => {
491            return Err(RuleNotApplicable);
492        }
493    }
494
495    if !(matches!(div, Expr::SafeMod(_, _, _))) {
496        return Err(RuleNotApplicable);
497    }
498
499    let children = div.children();
500    let a: &Atom = (&children[0]).try_into().or(Err(RuleNotApplicable))?;
501    let b: &Atom = (&children[1]).try_into().or(Err(RuleNotApplicable))?;
502
503    Ok(Reduction::pure(Expr::MinionModuloEqUndefZero(
504        meta.clone_dirty(),
505        a.clone(),
506        b.clone(),
507        val,
508    )))
509}
510
511#[register_rule(("Minion", 4400))]
512fn introduce_abseq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
513    let (x, abs_y): (Atom, Expr) = match expr.clone() {
514        Expr::Eq(_, a, b) => {
515            let a_atom: Option<&Atom> = (&*a).try_into().ok();
516            let b_atom: Option<&Atom> = (&*b).try_into().ok();
517
518            if let Some(a_atom) = a_atom {
519                Ok((a_atom.clone(), *b))
520            } else if let Some(b_atom) = b_atom {
521                Ok((b_atom.clone(), *a))
522            } else {
523                Err(RuleNotApplicable)
524            }
525        }
526
527        Expr::AuxDeclaration(_, a, b) => Ok((a.into(), *b)),
528
529        _ => Err(RuleNotApplicable),
530    }?;
531
532    let Expr::Abs(_, y) = abs_y else {
533        return Err(RuleNotApplicable);
534    };
535
536    let y: Atom = (*y).try_into().or(Err(RuleNotApplicable))?;
537
538    Ok(Reduction::pure(Expr::FlatAbsEq(Metadata::new(), x, y)))
539}
540
541/// Introduces a `MinionPowEq` constraint from a `SafePow`
542#[register_rule(("Minion", 4200))]
543fn introduce_poweq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
544    let (a, b, total) = match expr.clone() {
545        Expr::Eq(_, e1, e2) => match (*e1, *e2) {
546            (Expr::Atomic(_, total), Expr::SafePow(_, a, b)) => Ok((a, b, total)),
547            (Expr::SafePow(_, a, b), Expr::Atomic(_, total)) => Ok((a, b, total)),
548            _ => Err(RuleNotApplicable),
549        },
550
551        Expr::AuxDeclaration(_, total, e) => match *e {
552            Expr::SafePow(_, a, b) => Ok((a, b, Atom::Reference(total))),
553            _ => Err(RuleNotApplicable),
554        },
555        _ => Err(RuleNotApplicable),
556    }?;
557
558    let a: Atom = (*a).try_into().or(Err(RuleNotApplicable))?;
559    let b: Atom = (*b).try_into().or(Err(RuleNotApplicable))?;
560
561    Ok(Reduction::pure(Expr::MinionPow(
562        Metadata::new(),
563        a,
564        b,
565        total,
566    )))
567}
568
569/// Introduces a `FlatAlldiff` constraint from an `AllDiff`
570#[register_rule(("Minion", 4200))]
571fn introduce_flat_alldiff(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
572    let Expr::AllDiff(_, es) = expr else {
573        return Err(RuleNotApplicable);
574    };
575
576    let es = es.clone().unwrap_list().ok_or(RuleNotApplicable)?;
577
578    let atoms = es
579        .into_iter()
580        .map(|e| match e {
581            Expr::Atomic(_, atom) => Ok(atom),
582            _ => Err(RuleNotApplicable),
583        })
584        .process_results(|iter| iter.collect_vec())?;
585
586    Ok(Reduction::pure(Expr::FlatAllDiff(Metadata::new(), atoms)))
587}
588
589/// Introduces a Minion `MinusEq` constraint from `x = -y`, where x and y are atoms.
590///
591/// ```text
592/// x = -y ~> MinusEq(x,y)
593///
594///   where x,y are atoms
595/// ```
596#[register_rule(("Minion", 4400))]
597fn introduce_minuseq_from_eq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
598    let Expr::Eq(_, a, b) = expr else {
599        return Err(RuleNotApplicable);
600    };
601
602    fn try_get_atoms(a: &Expr, b: &Expr) -> Option<(Atom, Atom)> {
603        let a: &Atom = a.try_into().ok()?;
604        let Expr::Neg(_, b) = b else {
605            return None;
606        };
607
608        let b: &Atom = b.try_into().ok()?;
609
610        Some((a.clone(), b.clone()))
611    }
612
613    let a = *a.clone();
614    let b = *b.clone();
615
616    // x = - y. Find this symmetrically (a = - b or b = -a)
617    let Some((x, y)) = try_get_atoms(&a, &b).or_else(|| try_get_atoms(&b, &a)) else {
618        return Err(RuleNotApplicable);
619    };
620
621    Ok(Reduction::pure(Expr::FlatMinusEq(Metadata::new(), x, y)))
622}
623
624/// Introduces a Minion `MinusEq` constraint from `x =aux -y`, where x and y are atoms.
625///
626/// ```text
627/// x =aux -y ~> MinusEq(x,y)
628///
629///   where x,y are atoms
630/// ```
631#[register_rule(("Minion", 4400))]
632fn introduce_minuseq_from_aux_decl(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
633    // a =aux -b
634    //
635    let Expr::AuxDeclaration(_, a, b) = expr else {
636        return Err(RuleNotApplicable);
637    };
638
639    let a = Atom::Reference(a.clone());
640
641    let Expr::Neg(_, b) = (**b).clone() else {
642        return Err(RuleNotApplicable);
643    };
644
645    let Ok(b) = b.try_into() else {
646        return Err(RuleNotApplicable);
647    };
648
649    Ok(Reduction::pure(Expr::FlatMinusEq(Metadata::new(), a, b)))
650}
651
652/// Converts an implication to either `ineq` or `reifyimply`
653///
654/// ```text
655/// x -> y ~> ineq(x,y,0)
656/// where x is atomic, y is atomic
657///
658/// x -> y ~> reifyimply(y,x)
659/// where x is atomic, y is non-atomic
660/// ```
661#[register_rule(("Minion", 4400))]
662fn introduce_reifyimply_ineq_from_imply(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
663    let Expr::Imply(_, x, y) = expr else {
664        return Err(RuleNotApplicable);
665    };
666
667    let x_atom: &Atom = x.as_ref().try_into().or(Err(RuleNotApplicable))?;
668
669    // if both x and y are atoms,  x -> y ~> ineq(x,y,0)
670    //
671    // if only x is an atom, x -> y ~> reifyimply(y,x)
672    if let Ok(y_atom) = TryInto::<&Atom>::try_into(y.as_ref()) {
673        Ok(Reduction::pure(Expr::FlatIneq(
674            Metadata::new(),
675            x_atom.clone(),
676            y_atom.clone(),
677            0.into(),
678        )))
679    } else {
680        Ok(Reduction::pure(Expr::MinionReifyImply(
681            Metadata::new(),
682            y.clone(),
683            x_atom.clone(),
684        )))
685    }
686}
687
688/// Converts `__inDomain(a,domain) to w-inintervalset.
689///
690/// This applies if domain is integer and finite.
691#[register_rule(("Minion", 4400))]
692fn introduce_wininterval_set_from_indomain(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
693    let Expr::InDomain(_, e, domain) = expr else {
694        return Err(RuleNotApplicable);
695    };
696
697    let Expr::Atomic(_, atom @ Atom::Reference(_)) = e.as_ref() else {
698        return Err(RuleNotApplicable);
699    };
700
701    let Domain::IntDomain(ranges) = domain else {
702        return Err(RuleNotApplicable);
703    };
704
705    let mut out_ranges = vec![];
706
707    for range in ranges {
708        match range {
709            crate::ast::Range::Single(x) => {
710                out_ranges.push(*x);
711                out_ranges.push(*x);
712            }
713            crate::ast::Range::Bounded(x, y) => {
714                out_ranges.push(*x);
715                out_ranges.push(*y);
716            }
717            crate::ast::Range::UnboundedR(_) | crate::ast::Range::UnboundedL(_) => {
718                return Err(RuleNotApplicable);
719            }
720        }
721    }
722
723    Ok(Reduction::pure(Expr::MinionWInIntervalSet(
724        Metadata::new(),
725        atom.clone(),
726        out_ranges,
727    )))
728}
729
730/// Converts `[....][i]` to `element_one` if:
731///
732/// 1. the subject is a list literal
733/// 2. the subject is one dimensional
734#[register_rule(("Minion", 4400))]
735fn introduce_element_from_index(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
736    let (equalto, subject, indices) = match expr.clone() {
737        Expr::Eq(_, e1, e2) => match (*e1, *e2) {
738            (Expr::Atomic(_, eq), Expr::SafeIndex(_, subject, indices)) => {
739                Ok((eq, subject, indices))
740            }
741            (Expr::SafeIndex(_, subject, indices), Expr::Atomic(_, eq)) => {
742                Ok((eq, subject, indices))
743            }
744            _ => Err(RuleNotApplicable),
745        },
746        Expr::AuxDeclaration(_, name, expr) => match *expr {
747            Expr::SafeIndex(_, subject, indices) => Ok((Atom::Reference(name), subject, indices)),
748            _ => Err(RuleNotApplicable),
749        },
750        _ => Err(RuleNotApplicable),
751    }?;
752
753    if indices.len() != 1 {
754        return Err(RuleNotApplicable);
755    }
756
757    let Some(list) = subject.unwrap_list() else {
758        return Err(RuleNotApplicable);
759    };
760
761    let Expr::Atomic(_, index) = indices[0].clone() else {
762        return Err(RuleNotApplicable);
763    };
764
765    let mut atom_list = vec![];
766
767    for elem in list {
768        let Expr::Atomic(_, elem) = elem else {
769            return Err(RuleNotApplicable);
770        };
771
772        atom_list.push(elem);
773    }
774
775    Ok(Reduction::pure(Expr::MinionElementOne(
776        Metadata::new(),
777        atom_list,
778        index,
779        equalto,
780    )))
781}
782
783/// Flattens an implication.
784///
785/// ```text
786/// e -> y  (where e is non atomic)
787///  ~~>
788/// __0 -> y,
789///
790/// with new top level constraints
791/// __0 =aux x
792///
793/// ```
794///
795/// Unlike other expressions, only the left hand side of implications are flattened. This is
796/// because implications can be expressed as a `reifyimply` constraint, which takes a constraint as
797/// an argument:
798///
799/// ``` text
800/// r -> c ~> refifyimply(r,c)
801///  where r is an atom, c is a constraint
802/// ```
803///
804/// See [`introduce_reifyimply_ineq_from_imply`].
805#[register_rule(("Minion", 4200))]
806fn flatten_imply(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
807    let Expr::Imply(meta, x, y) = expr else {
808        return Err(RuleNotApplicable);
809    };
810
811    // flatten x
812    let aux_var_info = to_aux_var(x.as_ref(), symbols).ok_or(RuleNotApplicable)?;
813
814    let symbols = aux_var_info.symbols();
815    let new_x = aux_var_info.as_expr();
816
817    Ok(Reduction::new(
818        Expr::Imply(meta.clone(), Box::new(new_x), y.clone()),
819        vec![aux_var_info.top_level_expr()],
820        symbols,
821    ))
822}
823
824#[register_rule(("Minion", 4200))]
825fn flatten_generic(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
826    if !matches!(
827        expr,
828        Expr::SafeDiv(_, _, _)
829            | Expr::Neq(_, _, _)
830            | Expr::SafeMod(_, _, _)
831            | Expr::SafePow(_, _, _)
832            | Expr::Leq(_, _, _)
833            | Expr::Geq(_, _, _)
834            | Expr::Abs(_, _)
835            | Expr::Product(_, _)
836            | Expr::Neg(_, _)
837            | Expr::Not(_, _)
838            | Expr::SafeIndex(_, _, _)
839            | Expr::InDomain(_, _, _)
840    ) {
841        return Err(RuleNotApplicable);
842    }
843
844    let mut children = expr.children();
845
846    let mut symbols = symbols.clone();
847    let mut num_changed = 0;
848    let mut new_tops: Vec<Expr> = vec![];
849
850    for child in children.iter_mut() {
851        if let Some(aux_var_info) = to_aux_var(child, &symbols) {
852            symbols = aux_var_info.symbols();
853            new_tops.push(aux_var_info.top_level_expr());
854            *child = aux_var_info.as_expr();
855            num_changed += 1;
856        }
857    }
858
859    if num_changed == 0 {
860        return Err(RuleNotApplicable);
861    }
862
863    let expr = expr.with_children(children);
864
865    Ok(Reduction::new(expr, new_tops, symbols))
866}
867
868#[register_rule(("Minion", 4200))]
869fn flatten_eq(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
870    if !matches!(expr, Expr::Eq(_, _, _)) {
871        return Err(RuleNotApplicable);
872    }
873
874    let mut children = expr.children();
875    debug_assert_eq!(children.len(), 2);
876
877    let mut symbols = symbols.clone();
878    let mut num_changed = 0;
879    let mut new_tops: Vec<Expr> = vec![];
880
881    for child in children.iter_mut() {
882        if let Some(aux_var_info) = to_aux_var(child, &symbols) {
883            symbols = aux_var_info.symbols();
884            new_tops.push(aux_var_info.top_level_expr());
885            *child = aux_var_info.as_expr();
886            num_changed += 1;
887        }
888    }
889
890    // eq: both sides have to be non flat for the rule to be applicable!
891    if num_changed != 2 {
892        return Err(RuleNotApplicable);
893    }
894
895    let expr = expr.with_children(children);
896
897    Ok(Reduction::new(expr, new_tops, symbols))
898}
899
900/// Converts a Geq to an Ineq
901///
902/// ```text
903/// x >= y ~> y <= x + 0 ~> ineq(y,x,0)
904/// ```
905#[register_rule(("Minion", 4100))]
906fn geq_to_ineq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
907    let Expr::Geq(meta, e1, e2) = expr.clone() else {
908        return Err(RuleNotApplicable);
909    };
910
911    let Expr::Atomic(_, x) = *e1 else {
912        return Err(RuleNotApplicable);
913    };
914
915    let Expr::Atomic(_, y) = *e2 else {
916        return Err(RuleNotApplicable);
917    };
918
919    Ok(Reduction::pure(Expr::FlatIneq(
920        meta.clone_dirty(),
921        y,
922        x,
923        Lit::Int(0),
924    )))
925}
926
927/// Converts a Leq to an Ineq
928///
929/// ```text
930/// x <= y ~> x <= y + 0 ~> ineq(x,y,0)
931/// ```
932#[register_rule(("Minion", 4100))]
933fn leq_to_ineq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
934    let Expr::Leq(meta, e1, e2) = expr.clone() else {
935        return Err(RuleNotApplicable);
936    };
937
938    let Expr::Atomic(_, x) = *e1 else {
939        return Err(RuleNotApplicable);
940    };
941
942    let Expr::Atomic(_, y) = *e2 else {
943        return Err(RuleNotApplicable);
944    };
945
946    Ok(Reduction::pure(Expr::FlatIneq(
947        meta.clone_dirty(),
948        x,
949        y,
950        Lit::Int(0),
951    )))
952}
953
954// TODO: add this rule for geq
955
956/// ```text
957/// x <= y + k ~> ineq(x,y,k)
958/// ```
959#[register_rule(("Minion",4500))]
960fn x_leq_y_plus_k_to_ineq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
961    let Expr::Leq(meta, e1, e2) = expr.clone() else {
962        return Err(RuleNotApplicable);
963    };
964
965    let Expr::Atomic(_, x) = *e1 else {
966        return Err(RuleNotApplicable);
967    };
968
969    let Expr::Sum(_, sum_exprs) = *e2 else {
970        return Err(RuleNotApplicable);
971    };
972
973    let sum_exprs = sum_exprs.unwrap_list().ok_or(RuleNotApplicable)?;
974    let (y, k) = match sum_exprs.as_slice() {
975        [Expr::Atomic(_, y), Expr::Atomic(_, Atom::Literal(k))] => (y, k),
976        [Expr::Atomic(_, Atom::Literal(k)), Expr::Atomic(_, y)] => (y, k),
977        _ => {
978            return Err(RuleNotApplicable);
979        }
980    };
981
982    Ok(Reduction::pure(Expr::FlatIneq(
983        meta.clone_dirty(),
984        x,
985        y.clone(),
986        k.clone(),
987    )))
988}
989
990/// ```text
991/// y + k >= x ~> ineq(x,y,k)
992/// ```
993#[register_rule(("Minion",4800))]
994fn y_plus_k_geq_x_to_ineq(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
995    // impl same as x_leq_y_plus_k but with lhs and rhs flipped
996    let Expr::Geq(meta, e2, e1) = expr.clone() else {
997        return Err(RuleNotApplicable);
998    };
999
1000    let Expr::Atomic(_, x) = *e1 else {
1001        return Err(RuleNotApplicable);
1002    };
1003
1004    let Expr::Sum(_, sum_exprs) = *e2 else {
1005        return Err(RuleNotApplicable);
1006    };
1007
1008    let sum_exprs = sum_exprs.unwrap_list().ok_or(RuleNotApplicable)?;
1009    let (y, k) = match sum_exprs.as_slice() {
1010        [Expr::Atomic(_, y), Expr::Atomic(_, Atom::Literal(k))] => (y, k),
1011        [Expr::Atomic(_, Atom::Literal(k)), Expr::Atomic(_, y)] => (y, k),
1012        _ => {
1013            return Err(RuleNotApplicable);
1014        }
1015    };
1016
1017    Ok(Reduction::pure(Expr::FlatIneq(
1018        meta.clone_dirty(),
1019        x,
1020        y.clone(),
1021        k.clone(),
1022    )))
1023}
1024
1025/// Flattening rule for not(bool_lit)
1026///
1027/// For some boolean variable x:
1028/// ```text
1029///  not(x)      ~>  w-literal(x,0)
1030/// ```
1031///
1032/// ## Rationale
1033///
1034/// Minion's watched-and and watched-or constraints only takes other constraints as arguments.
1035///
1036/// This restates boolean variables as the equivalent constraint "SAT if x is true".
1037///
1038/// The regular bool_lit case is dealt with directly by the Minion solver interface (as it is a
1039/// trivial match).
1040
1041#[register_rule(("Minion", 4100))]
1042fn not_literal_to_wliteral(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
1043    use Domain::BoolDomain;
1044    match expr {
1045        Expr::Not(m, expr) => {
1046            if let Expr::Atomic(_, Atom::Reference(name)) = (**expr).clone() {
1047                if symbols
1048                    .domain(&name)
1049                    .is_some_and(|x| matches!(x, BoolDomain))
1050                {
1051                    return Ok(Reduction::pure(Expr::FlatWatchedLiteral(
1052                        m.clone_dirty(),
1053                        name.clone(),
1054                        Lit::Bool(false),
1055                    )));
1056                }
1057            }
1058            Err(RuleNotApplicable)
1059        }
1060        _ => Err(RuleNotApplicable),
1061    }
1062}
1063
1064/// Flattening rule for not(X) in Minion, where X is a constraint.
1065///
1066/// ```text
1067/// not(X) ~> reify(X,0)
1068/// ```
1069///
1070/// This rule has lower priority than boolean_literal_to_wliteral so that we can assume that the
1071/// nested expressions are constraints not variables.
1072
1073#[register_rule(("Minion", 4090))]
1074fn not_constraint_to_reify(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
1075    if !matches!(expr, Expr::Not(_,c) if !matches!(**c, Expr::Atomic(_,_))) {
1076        return Err(RuleNotApplicable);
1077    }
1078
1079    let Expr::Not(m, e) = expr else {
1080        unreachable!();
1081    };
1082
1083    extra_check! {
1084        if !is_flat(e) {
1085            return Err(RuleNotApplicable);
1086        }
1087    };
1088
1089    Ok(Reduction::pure(Expr::MinionReify(
1090        m.clone(),
1091        e.clone(),
1092        Atom::Literal(Lit::Bool(false)),
1093    )))
1094}
1095
1096/// Converts an equality to a boolean into a `reify` constraint.
1097///
1098/// ```text
1099/// x =aux c ~> reify(c,x)
1100/// x = c ~> reify(c,x)
1101///
1102/// where c is a boolean constraint
1103/// ```
1104#[register_rule(("Minion", 4400))]
1105fn bool_eq_to_reify(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
1106    let (atom, e): (Atom, Box<Expr>) = match expr {
1107        Expr::AuxDeclaration(_, name, e) => Ok((name.clone().into(), e.clone())),
1108        Expr::Eq(_, a, b) => match (a.as_ref(), b.as_ref()) {
1109            (Expr::Atomic(_, atom), _) => Ok((atom.clone(), b.clone())),
1110            (_, Expr::Atomic(_, atom)) => Ok((atom.clone(), a.clone())),
1111            _ => Err(RuleNotApplicable),
1112        },
1113
1114        _ => Err(RuleNotApplicable),
1115    }?;
1116
1117    // e does not have to be valid minion constraint yet, as long as we know it can turn into one
1118    // (i.e. it is boolean).
1119    let Some(ReturnType::Bool) = e.as_ref().return_type() else {
1120        return Err(RuleNotApplicable);
1121    };
1122
1123    Ok(Reduction::pure(Expr::MinionReify(
1124        Metadata::new(),
1125        e.clone(),
1126        atom,
1127    )))
1128}