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