1use std::collections::HashSet;
2
3use indexmap::IndexMap;
4
5use crate::ast::Typeable;
6use crate::{
7 ast::{
8 AbstractLiteral, Atom, DomainPtr, Expression as Expr, GroundDomain, Literal as Lit,
9 Metadata, Moo, Range, ReturnType,
10 },
11 into_matrix_expr,
12 rule_engine::{ApplicationError::RuleNotApplicable, ApplicationResult, RuleEffect},
13};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20enum ConstantBoundOp {
21 Gt,
23 Geq,
25 Lt,
27 Leq,
29}
30
31impl ConstantBoundOp {
32 fn prefers_larger_rhs(self) -> bool {
34 matches!(self, ConstantBoundOp::Gt | ConstantBoundOp::Geq)
35 }
36}
37
38fn normalise_int_domain(domain: &GroundDomain) -> GroundDomain {
40 match domain {
41 GroundDomain::Int(ranges) => GroundDomain::Int(Range::squeeze(
42 &ranges
43 .iter()
44 .map(|range| Range::new(range.low().copied(), range.high().copied()))
45 .collect::<Vec<_>>(),
46 )),
47 _ => domain.clone(),
48 }
49}
50
51fn is_semantically_safe(expr: &Expr) -> bool {
53 fn helper(expr: &Expr, resolving: &mut HashSet<crate::ast::serde::ObjId>) -> bool {
54 if matches!(
55 expr,
56 Expr::UnsafeDiv(_, _, _)
57 | Expr::UnsafeMod(_, _, _)
58 | Expr::UnsafePow(_, _, _)
59 | Expr::UnsafeIndex(_, _, _)
60 | Expr::Bubble(_, _, _)
61 | Expr::UnsafeSlice(_, _, _)
62 ) {
63 return false;
64 }
65
66 if let Expr::Image(_, subject, argument) = expr
69 && crate::ast::expressions::image_can_be_undefined(subject, argument)
70 {
71 return false;
72 }
73
74 if let Expr::Atomic(_, Atom::Reference(reference)) = expr {
75 let id = reference.id();
76 if !resolving.insert(id.clone()) {
77 return false;
78 }
79 let is_safe = reference
80 .with_resolved_expression(|resolved| helper(resolved, resolving))
81 .unwrap_or(true);
82 resolving.remove(&id);
83 return is_safe;
84 }
85
86 let mut is_safe = true;
87 expr.for_each_expr_child(&mut |child| {
88 if is_safe {
89 is_safe = helper(child, resolving);
90 }
91 });
92 is_safe
93 }
94
95 helper(expr, &mut HashSet::new())
96}
97
98fn simplify_in_domain(expr: &Expr, domain: &DomainPtr) -> Option<bool> {
100 if !is_semantically_safe(expr) {
101 return None;
102 }
103
104 let expr_domain = resolved_ground_domain_of_for_partial_eval(expr)?;
105 let domain = domain.resolve().ok()?;
106 let intersection = expr_domain.intersect(&domain).ok()?;
107
108 if normalise_int_domain(&intersection) == normalise_int_domain(expr_domain.as_ref()) {
109 return Some(true);
110 }
111
112 if let Ok(values_in_domain) = intersection.values_i32()
113 && values_in_domain.is_empty()
114 {
115 return Some(false);
116 }
117
118 None
119}
120
121fn singleton_int_value(expr: &Expr) -> Option<i32> {
123 if let Ok(value) = expr.try_into() {
124 return Some(value);
125 }
126
127 let domain = resolved_ground_domain_of_for_partial_eval(expr)?;
128 let GroundDomain::Int(ranges) = domain.as_ref() else {
129 return None;
130 };
131 let [range] = ranges.as_slice() else {
132 return None;
133 };
134 let (Some(low), Some(high)) = (range.low(), range.high()) else {
135 return None;
136 };
137
138 if low == high { Some(*low) } else { None }
139}
140
141fn cheap_singleton_int_value(expr: &Expr) -> Option<i32> {
148 match expr {
149 Expr::Atomic(_, Atom::Literal(Lit::Int(value))) => Some(*value),
150 Expr::Atomic(_, Atom::Reference(reference)) => {
151 let domain = reference.domain()?.resolve().ok()?;
152 let GroundDomain::Int(ranges) = domain.as_ref() else {
153 return None;
154 };
155 let [range] = ranges.as_slice() else {
156 return None;
157 };
158 let (Some(low), Some(high)) = (range.low(), range.high()) else {
159 return None;
160 };
161 (low == high).then_some(*low)
162 }
163 _ => None,
164 }
165}
166
167fn matrix_index_offset(index_domain: &DomainPtr, index: i32) -> Option<usize> {
168 let ranges = index_domain.as_int_ground()?;
169 let [range] = ranges.as_slice() else {
170 return None;
171 };
172 let from = *range.low()?;
173 usize::try_from(index.checked_sub(from)?).ok()
174}
175
176fn ground_matrix_index_offset(index_domain: &GroundDomain, index: i32) -> Option<usize> {
177 let GroundDomain::Int(ranges) = index_domain else {
178 return None;
179 };
180 let [range] = ranges.as_slice() else {
181 return None;
182 };
183 let from = *range.low()?;
184 usize::try_from(index.checked_sub(from)?).ok()
185}
186
187fn resolve_matrix_element(subject: &Expr, index: i32) -> Option<Expr> {
192 match subject {
193 Expr::TypeAnnotation(_, inner, _) | Expr::DomainAnnotation(_, inner, _) => {
194 resolve_matrix_element(inner, index)
195 }
196 Expr::AbstractLiteral(_, AbstractLiteral::Matrix(elems, index_domain)) => elems
197 .get(matrix_index_offset(index_domain, index)?)
198 .cloned(),
199 Expr::Atomic(
200 _,
201 Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Matrix(elems, index_domain))),
202 ) => elems
203 .get(ground_matrix_index_offset(index_domain, index)?)
204 .cloned()
205 .map(|literal| Expr::Atomic(Metadata::new(), Atom::Literal(literal))),
206 Expr::Atomic(_, Atom::Reference(reference)) => reference
207 .with_resolved_expression(|resolved| resolve_matrix_element(resolved, index))
208 .flatten()
209 .or_else(|| {
210 let Lit::AbstractLiteral(AbstractLiteral::Matrix(elems, index_domain)) =
213 reference.resolve_constant()?
214 else {
215 return None;
216 };
217 elems
218 .get(ground_matrix_index_offset(&index_domain, index)?)
219 .cloned()
220 .map(|literal| Expr::Atomic(Metadata::new(), Atom::Literal(literal)))
221 }),
222 _ => None,
223 }
224}
225
226fn resolved_ground_domain_of_for_partial_eval(expr: &Expr) -> Option<Moo<GroundDomain>> {
228 match expr {
229 Expr::SafeIndex(_, subject, _) => {
230 let subject_domain = resolved_ground_domain_of_for_partial_eval(subject)?;
231 let GroundDomain::Matrix(elem_domain, _) = subject_domain.as_ref() else {
232 return None;
233 };
234
235 Some(elem_domain.clone())
236 }
237 Expr::SafeSlice(_, subject, indices) => {
238 let subject_domain = resolved_ground_domain_of_for_partial_eval(subject)?;
239 let GroundDomain::Matrix(elem_domain, index_domains) = subject_domain.as_ref() else {
240 return None;
241 };
242 let sliced_dimension = indices.iter().position(Option::is_none);
243
244 match sliced_dimension {
245 Some(dimension) => Some(Moo::new(GroundDomain::Matrix(
246 elem_domain.clone(),
247 vec![index_domains[dimension].clone()],
248 ))),
249 None => Some(elem_domain.clone()),
250 }
251 }
252 Expr::UnsafeIndex(_, _, _) | Expr::UnsafeSlice(_, _, _) => None,
253 _ => expr.domain_of()?.resolve().ok(),
254 }
255}
256
257fn is_empty_min_max(expr: &Expr) -> bool {
263 matches!(expr, Expr::Min(_, values) | Expr::Max(_, values) if is_empty_matrix_operand(values))
264}
265
266fn can_be_undefined_but_not_empty(expr: &Expr) -> bool {
270 matches!(expr, Expr::Image(_, _, _))
271}
272
273fn simplify_comparison_with_literal(
279 expr: &Expr,
280 lit: &Lit,
281) -> Option<(Option<bool>, Option<bool>)> {
282 let is_safe = is_semantically_safe(expr);
288 if !is_safe && !is_empty_min_max(expr) && !can_be_undefined_but_not_empty(expr) {
289 return None;
290 }
291
292 let expr_domain = resolved_ground_domain_of_for_partial_eval(expr)?;
293
294 if matches!(expr_domain.as_ref(), GroundDomain::Empty(_)) {
297 return Some((Some(false), Some(false)));
298 }
299
300 if !expr_domain.contains(lit).ok()? {
301 if let Expr::Atomic(_, Atom::Reference(reference)) = expr
305 && !reference.ptr().reprs().is_empty()
306 {
307 return None;
308 }
309 return Some((Some(false), is_safe.then_some(true)));
313 }
314
315 if !is_safe {
316 return None;
317 }
318
319 match (expr_domain.as_ref(), lit) {
320 (GroundDomain::Bool, Lit::Bool(_)) => None,
321 (GroundDomain::Int(ranges), Lit::Int(value)) => {
322 let [range] = ranges.as_slice() else {
323 return None;
324 };
325 let (Some(low), Some(high)) = (range.low(), range.high()) else {
326 return None;
327 };
328
329 if low == high && low == value {
330 Some((Some(true), Some(false)))
331 } else {
332 None
333 }
334 }
335 _ => None,
336 }
337}
338
339fn comparison_domain_lookup_is_cheap(expr: &Expr, mode: PartialEvalMode) -> bool {
345 mode == PartialEvalMode::Deep
346 || matches!(expr, Expr::Atomic(_, Atom::Reference(_)))
347 || is_empty_min_max(expr)
348}
349
350fn simplify_reflexive_comparison(x: &Expr, y: &Expr) -> Option<(bool, bool)> {
352 if x.identical_atom_to(y) && is_semantically_safe(x) && is_semantically_safe(y) {
353 return Some((true, false));
354 }
355
356 if is_semantically_safe(x) && is_semantically_safe(y) && x == y {
357 return Some((true, false));
358 }
359
360 None
361}
362
363fn simplify_reflexive_comparison_with_mode(
364 x: &Expr,
365 y: &Expr,
366 mode: PartialEvalMode,
367) -> Option<(bool, bool)> {
368 match mode {
369 PartialEvalMode::Deep => simplify_reflexive_comparison(x, y),
370 PartialEvalMode::Local => x.identical_atom_to(y).then_some((true, false)),
371 }
372}
373
374pub fn run_partial_evaluator(expr: &Expr) -> ApplicationResult {
375 run_partial_evaluator_with_mode(expr, PartialEvalMode::Deep)
376}
377
378pub fn run_partial_evaluator_local(expr: &Expr) -> ApplicationResult {
384 run_partial_evaluator_with_mode(expr, PartialEvalMode::Local)
385}
386
387#[derive(Clone, Copy, Debug, PartialEq, Eq)]
388enum PartialEvalMode {
389 Deep,
390 Local,
391}
392
393pub fn try_lower_bool_atom_eq_true(expr: &Expr) -> Option<Expr> {
402 let Expr::Eq(_, left, right) = expr else {
403 return None;
404 };
405
406 let atom = match (left.as_ref(), right.as_ref()) {
407 (Expr::Atomic(_, Atom::Literal(Lit::Bool(true))), Expr::Atomic(_, atom))
408 if !matches!(atom, Atom::Literal(_)) =>
409 {
410 right.as_ref()
411 }
412 (Expr::Atomic(_, atom), Expr::Atomic(_, Atom::Literal(Lit::Bool(true))))
413 if !matches!(atom, Atom::Literal(_)) =>
414 {
415 left.as_ref()
416 }
417 _ => return None,
418 };
419
420 if atom.return_type() != ReturnType::Bool {
421 return None;
422 }
423
424 Some(atom.clone())
425}
426
427fn run_partial_evaluator_with_mode(expr: &Expr, mode: PartialEvalMode) -> ApplicationResult {
428 match expr {
432 Expr::Difference(_, _, _) => Err(RuleNotApplicable),
433 Expr::Union(_, _, _) => Err(RuleNotApplicable),
434 Expr::In(_, _, _) => Err(RuleNotApplicable),
435 Expr::Intersect(_, _, _) => Err(RuleNotApplicable),
436 Expr::Supset(_, _, _) => Err(RuleNotApplicable),
437 Expr::SupsetEq(_, _, _) => Err(RuleNotApplicable),
438 Expr::Subset(_, _, _) => Err(RuleNotApplicable),
439 Expr::SubsetEq(_, _, _) => Err(RuleNotApplicable),
440 Expr::AbstractLiteral(_, _) => Err(RuleNotApplicable),
441 Expr::CatchUndef(_, _, _) => Err(RuleNotApplicable),
442 Expr::Comprehension(_, _) => Err(RuleNotApplicable),
443 Expr::DominanceRelation(_, _) => Err(RuleNotApplicable),
444 Expr::TypeAnnotation(_, _, _) => Err(RuleNotApplicable),
445 Expr::DomainAnnotation(_, _, _) => Err(RuleNotApplicable),
446 Expr::FromSolution(_, _) => Err(RuleNotApplicable),
447 Expr::Metavar(_, _) => Err(RuleNotApplicable),
448 Expr::UnsafeIndex(_, _, _) => Err(RuleNotApplicable),
449 Expr::UnsafeSlice(_, _, _) => Err(RuleNotApplicable),
450 Expr::Table(_, _, _) => Err(RuleNotApplicable),
451 Expr::NegativeTable(_, _, _) => Err(RuleNotApplicable),
452 Expr::AtLeast(_, _, _, _) => Err(RuleNotApplicable),
453 Expr::AtMost(_, _, _, _) => Err(RuleNotApplicable),
454 Expr::Gcc(_, _, _, _) | Expr::GccWeak(_, _, _, _) => Err(RuleNotApplicable),
455 Expr::RecordField(_, _, _) => Err(RuleNotApplicable),
456 Expr::AttributeAsConstraint(_, _, _, _) => Err(RuleNotApplicable),
457 Expr::SafeIndex(_, subject, indices) => {
458 if indices.is_empty() {
460 return Err(RuleNotApplicable);
461 }
462
463 let index = singleton_int_value(&indices[0]).ok_or(RuleNotApplicable)?;
465
466 let selected = resolve_matrix_element(subject, index).ok_or(RuleNotApplicable)?;
467 if indices.len() == 1 {
468 Ok(RuleEffect::pure(selected))
469 } else {
470 Ok(RuleEffect::pure(Expr::SafeIndex(
471 Metadata::new(),
472 Moo::new(selected),
473 indices[1..].to_vec(),
474 )))
475 }
476 }
477 Expr::SafeSlice(_, _, _) => Err(RuleNotApplicable),
478 Expr::InDomain(_, x, domain) => {
479 if mode == PartialEvalMode::Deep
480 && let Some(result) = simplify_in_domain(x, domain)
481 {
482 Ok(RuleEffect::pure(Expr::Atomic(
483 Metadata::new(),
484 result.into(),
485 )))
486 } else if let Expr::Atomic(_, Atom::Reference(decl)) = x.as_ref() {
487 let decl_domain = decl
488 .domain()
489 .ok_or(RuleNotApplicable)?
490 .resolve()
491 .map_err(|_| RuleNotApplicable)?;
492 let domain = domain.resolve().map_err(|_| RuleNotApplicable)?;
493
494 let intersection = decl_domain
495 .intersect(&domain)
496 .map_err(|_| RuleNotApplicable)?;
497
498 if &intersection == decl_domain.as_ref() {
500 Ok(RuleEffect::pure(Expr::Atomic(Metadata::new(), true.into())))
501 }
502 else if let Ok(values_in_domain) = intersection.values_i32()
510 && values_in_domain.is_empty()
511 {
512 Ok(RuleEffect::pure(Expr::Atomic(
513 Metadata::new(),
514 false.into(),
515 )))
516 } else {
517 Err(RuleNotApplicable)
518 }
519 } else if let Expr::Atomic(_, Atom::Literal(lit)) = x.as_ref() {
520 if domain
521 .resolve()
522 .and_then(|gd| gd.contains(lit))
523 .map_err(|_| RuleNotApplicable)?
524 {
525 Ok(RuleEffect::pure(Expr::Atomic(Metadata::new(), true.into())))
526 } else {
527 Ok(RuleEffect::pure(Expr::Atomic(
528 Metadata::new(),
529 false.into(),
530 )))
531 }
532 } else {
533 Err(RuleNotApplicable)
534 }
535 }
536 Expr::Bubble(_, expr, cond) => {
537 if let Expr::Atomic(_, Atom::Literal(Lit::Bool(true))) = cond.as_ref() {
541 Ok(RuleEffect::pure(Moo::unwrap_or_clone(expr.clone())))
542 } else {
543 Err(RuleNotApplicable)
544 }
545 }
546 Expr::Atomic(_, _) => Err(RuleNotApplicable),
547 Expr::ToInt(_, expression) => {
548 if expression.return_type() == ReturnType::Int {
549 Ok(RuleEffect::pure(Moo::unwrap_or_clone(expression.clone())))
550 } else {
551 Err(RuleNotApplicable)
552 }
553 }
554 Expr::Abs(m, e) => match e.as_ref() {
555 Expr::Neg(_, inner) => Ok(RuleEffect::pure(Expr::Abs(m.clone(), inner.clone()))),
556 _ => Err(RuleNotApplicable),
557 },
558 Expr::Sum(m, vec) => {
559 let vec = vec.unwrap_list_cow().ok_or(RuleNotApplicable)?;
560 let mut acc = 0;
561 let mut n_consts = 0;
562 for expr in vec.iter() {
563 if let Expr::Atomic(_, Atom::Literal(Lit::Int(x))) = expr {
564 acc += *x;
565 n_consts += 1;
566 }
567 }
568
569 if n_consts <= 1 {
570 return Err(RuleNotApplicable);
571 }
572
573 let mut new_vec: Vec<Expr> = vec
574 .iter()
575 .filter(|expr| !matches!(expr, Expr::Atomic(_, Atom::Literal(Lit::Int(_)))))
576 .cloned()
577 .collect();
578 if acc != 0 {
579 new_vec.push(Expr::Atomic(
580 Default::default(),
581 Atom::Literal(Lit::Int(acc)),
582 ));
583 }
584
585 Ok(RuleEffect::pure(Expr::Sum(
586 m.clone(),
587 Moo::new(into_matrix_expr![new_vec]),
588 )))
589 }
590
591 Expr::Product(m, vec) => {
592 let mut acc = 1;
593 let mut n_consts = 0;
594 let vec = vec.unwrap_list_cow().ok_or(RuleNotApplicable)?;
595 for expr in vec.iter() {
596 if let Expr::Atomic(_, Atom::Literal(Lit::Int(x))) = expr {
597 acc *= *x;
598 n_consts += 1;
599 }
600 }
601
602 if n_consts == 0 {
603 return Err(RuleNotApplicable);
604 }
605
606 if acc == 0 && mode == PartialEvalMode::Local {
607 return Err(RuleNotApplicable);
608 }
609
610 if acc != 0 && n_consts == 1 {
611 return Err(RuleNotApplicable);
612 }
613
614 let mut new_vec: Vec<Expr> = vec
615 .iter()
616 .filter(|expr| !matches!(expr, Expr::Atomic(_, Atom::Literal(Lit::Int(_)))))
617 .cloned()
618 .collect();
619
620 new_vec.push(Expr::Atomic(
621 Default::default(),
622 Atom::Literal(Lit::Int(acc)),
623 ));
624 let new_product = Expr::Product(m.clone(), Moo::new(into_matrix_expr![new_vec]));
625
626 if acc == 0 {
627 if mode == PartialEvalMode::Deep && is_semantically_safe(&new_product) {
631 Ok(RuleEffect::pure(Expr::Atomic(
632 Default::default(),
633 Atom::Literal(Lit::Int(0)),
634 )))
635 } else {
636 Err(RuleNotApplicable)
637 }
638 } else {
639 Ok(RuleEffect::pure(new_product))
641 }
642 }
643
644 Expr::Min(m, e) => {
645 let Some(vec) = e.unwrap_list_cow() else {
646 return Err(RuleNotApplicable);
647 };
648 let mut acc: Option<i32> = None;
649 let mut n_consts = 0;
650 for expr in vec.iter() {
651 if let Expr::Atomic(_, Atom::Literal(Lit::Int(x))) = expr {
652 n_consts += 1;
653 acc = match acc {
654 Some(i) => {
655 if i > *x {
656 Some(*x)
657 } else {
658 Some(i)
659 }
660 }
661 None => Some(*x),
662 };
663 }
664 }
665
666 if n_consts <= 1 {
667 return Err(RuleNotApplicable);
668 }
669
670 let mut new_vec: Vec<Expr> = vec
671 .iter()
672 .filter(|expr| !matches!(expr, Expr::Atomic(_, Atom::Literal(Lit::Int(_)))))
673 .cloned()
674 .collect();
675 if let Some(i) = acc {
676 new_vec.push(Expr::Atomic(Default::default(), Atom::Literal(Lit::Int(i))));
677 }
678
679 Ok(RuleEffect::pure(Expr::Min(
680 m.clone(),
681 Moo::new(into_matrix_expr![new_vec]),
682 )))
683 }
684
685 Expr::Max(m, e) => {
686 let Some(vec) = e.unwrap_list_cow() else {
687 return Err(RuleNotApplicable);
688 };
689
690 let mut acc: Option<i32> = None;
691 let mut n_consts = 0;
692 for expr in vec.iter() {
693 if let Expr::Atomic(_, Atom::Literal(Lit::Int(x))) = expr {
694 n_consts += 1;
695 acc = match acc {
696 Some(i) => {
697 if i < *x {
698 Some(*x)
699 } else {
700 Some(i)
701 }
702 }
703 None => Some(*x),
704 };
705 }
706 }
707
708 if n_consts <= 1 {
709 return Err(RuleNotApplicable);
710 }
711
712 let mut new_vec: Vec<Expr> = vec
713 .iter()
714 .filter(|expr| !matches!(expr, Expr::Atomic(_, Atom::Literal(Lit::Int(_)))))
715 .cloned()
716 .collect();
717 if let Some(i) = acc {
718 new_vec.push(Expr::Atomic(Default::default(), Atom::Literal(Lit::Int(i))));
719 }
720
721 Ok(RuleEffect::pure(Expr::Max(
722 m.clone(),
723 Moo::new(into_matrix_expr![new_vec]),
724 )))
725 }
726 Expr::Not(_, e1) => {
727 let Expr::Imply(_, p, q) = e1.as_ref() else {
728 return Err(RuleNotApplicable);
729 };
730
731 if mode == PartialEvalMode::Deep && !is_semantically_safe(e1) {
732 return Err(RuleNotApplicable);
733 }
734
735 match (p.as_ref(), q.as_ref()) {
736 (_, Expr::Atomic(_, Atom::Literal(Lit::Bool(true)))) => {
737 Ok(RuleEffect::pure(Expr::from(false)))
738 }
739 (_, Expr::Atomic(_, Atom::Literal(Lit::Bool(false)))) => {
740 Ok(RuleEffect::pure(Moo::unwrap_or_clone(p.clone())))
741 }
742 (Expr::Atomic(_, Atom::Literal(Lit::Bool(true))), _) => {
743 Ok(RuleEffect::pure(Expr::Not(Metadata::new(), q.clone())))
744 }
745 (Expr::Atomic(_, Atom::Literal(Lit::Bool(false))), _) => {
746 Ok(RuleEffect::pure(Expr::from(false)))
747 }
748 _ => Err(RuleNotApplicable),
749 }
750 }
751 Expr::Or(m, e) => {
752 if is_empty_matrix_operand(e) {
754 return Ok(RuleEffect::pure(Expr::from(false)));
755 }
756
757 let Some(terms) = e.unwrap_list_cow() else {
758 return Err(RuleNotApplicable);
759 };
760
761 let mut has_changed = false;
762 let mut non_literal_terms = 0;
763
764 for expr in terms.iter() {
767 if let Expr::Atomic(_, Atom::Literal(Lit::Bool(x))) = expr {
768 has_changed = true;
769
770 if *x {
773 return Ok(RuleEffect::pure(true.into()));
774 }
775 } else {
776 non_literal_terms += 1;
777 }
778 }
779
780 if check_pairwise_or_tautologies(&terms) {
782 return Ok(RuleEffect::pure(true.into()));
783 }
784
785 if non_literal_terms == 0 {
787 return Ok(RuleEffect::pure(false.into()));
788 }
789
790 if !has_changed {
791 return Err(RuleNotApplicable);
792 }
793
794 let new_terms = terms
795 .iter()
796 .filter(|expr| !matches!(expr, Expr::Atomic(_, Atom::Literal(Lit::Bool(_)))))
797 .cloned()
798 .collect::<Vec<_>>();
799
800 Ok(RuleEffect::pure(Expr::Or(
801 m.clone(),
802 Moo::new(into_matrix_expr![new_terms]),
803 )))
804 }
805 Expr::And(_, e) => {
806 if is_empty_matrix_operand(e) {
813 return Ok(RuleEffect::pure(Expr::from(true)));
814 }
815
816 let Some(vec) = e.unwrap_list_cow() else {
817 return Err(RuleNotApplicable);
818 };
819
820 let mut has_changed: bool = false;
821 #[allow(clippy::mutable_key_type)]
824 let mut distinct_bounds: HashSet<(&Atom, ConstantBoundOp)> = HashSet::new();
825 let mut constant_bound_terms: usize = 0;
826 for expr in vec.iter() {
827 if let Expr::Atomic(_, Atom::Literal(Lit::Bool(x))) = expr {
828 if !x {
829 return Ok(RuleEffect::pure(Expr::Atomic(
830 Default::default(),
831 Atom::Literal(Lit::Bool(false)),
832 )));
833 }
834 has_changed = true;
835 } else if let Some((lhs, op, _)) = as_constant_bound_comparison_ref(expr) {
836 constant_bound_terms += 1;
837 distinct_bounds.insert((lhs, op));
838 }
839 }
840
841 if constant_bound_terms > distinct_bounds.len() {
843 has_changed = true;
844 }
845
846 if !has_changed {
847 return Err(RuleNotApplicable);
848 }
849 drop(distinct_bounds);
850
851 let mut new_vec: Vec<Expr> = Vec::new();
853 let mut constant_bounds: IndexMap<(Atom, ConstantBoundOp), i32> = IndexMap::new();
855 for expr in vec.iter() {
856 if matches!(expr, Expr::Atomic(_, Atom::Literal(Lit::Bool(_)))) {
857 continue;
858 } else if let Some((lhs, op, rhs)) = as_constant_bound_comparison(expr) {
859 merge_constant_bound(&mut constant_bounds, lhs, op, rhs);
860 } else {
861 new_vec.push(expr.clone());
862 }
863 }
864
865 for ((lhs, op), rhs) in constant_bounds {
866 new_vec.push(make_constant_bound_comparison(lhs, op, rhs));
867 }
868
869 if new_vec.is_empty() {
870 Ok(RuleEffect::pure(Expr::from(true)))
871 } else {
872 Ok(RuleEffect::pure(Expr::And(
873 Metadata::new(),
874 Moo::new(into_matrix_expr![new_vec]),
875 )))
876 }
877 }
878
879 Expr::Root(_, es) => {
881 match es.as_slice() {
882 [] => Err(RuleNotApplicable),
883 [Expr::And(_, _)] => Ok(()),
885 [_] => Err(RuleNotApplicable),
887 [_, _, ..] => Ok(()),
888 }?;
889
890 let mut new_vec: Vec<Expr> = Vec::new();
891 let mut has_changed: bool = false;
892 for expr in es {
893 match expr {
894 Expr::Atomic(_, Atom::Literal(Lit::Bool(x))) => {
895 has_changed = true;
896 if !x {
897 return Ok(RuleEffect::pure(Expr::Root(
899 Metadata::new(),
900 vec![Expr::Atomic(
901 Default::default(),
902 Atom::Literal(Lit::Bool(false)),
903 )],
904 )));
905 }
906 }
908
909 Expr::And(_, vecs) => match Moo::unwrap_or_clone(vecs.clone()).into_list() {
911 Some(list) => {
912 has_changed = true;
913 for conjunct in list {
914 match conjunct {
915 Expr::Atomic(_, Atom::Literal(Lit::Bool(false))) => {
916 return Ok(RuleEffect::pure(Expr::Root(
917 Metadata::new(),
918 vec![Expr::Atomic(
919 Default::default(),
920 Atom::Literal(Lit::Bool(false)),
921 )],
922 )));
923 }
924 Expr::Atomic(_, Atom::Literal(Lit::Bool(true))) => {}
925 other => new_vec.push(other),
926 }
927 }
928 }
929 None => new_vec.push(expr.clone()),
930 },
931 _ => new_vec.push(expr.clone()),
932 }
933 }
934
935 if !has_changed {
936 Err(RuleNotApplicable)
937 } else {
938 if new_vec.is_empty() {
939 new_vec.push(true.into());
940 }
941 Ok(RuleEffect::pure(Expr::Root(Metadata::new(), new_vec)))
942 }
943 }
944 Expr::Imply(_m, x, y) => {
945 if let Expr::Atomic(_, Atom::Literal(Lit::Bool(x))) = x.as_ref() {
946 return if *x {
947 Ok(RuleEffect::pure(Moo::unwrap_or_clone(y.clone())))
949 } else {
950 Ok(RuleEffect::pure(Expr::Atomic(Metadata::new(), true.into())))
952 };
953 };
954
955 if let Expr::Atomic(_, Atom::Literal(Lit::Bool(y))) = y.as_ref() {
956 return if *y {
957 Ok(RuleEffect::pure(Expr::from(true)))
959 } else {
960 Ok(RuleEffect::pure(Expr::Not(Metadata::new(), x.clone())))
962 };
963 };
964
965 if x.identical_atom_to(y.as_ref())
972 && (mode == PartialEvalMode::Local
973 || (is_semantically_safe(x) && is_semantically_safe(y)))
974 {
975 return Ok(RuleEffect::pure(true.into()));
976 }
977
978 Err(RuleNotApplicable)
979 }
980 Expr::Iff(_m, x, y) => {
981 if let Expr::Atomic(_, Atom::Literal(Lit::Bool(x))) = x.as_ref() {
982 return if *x {
983 Ok(RuleEffect::pure(Moo::unwrap_or_clone(y.clone())))
985 } else {
986 Ok(RuleEffect::pure(Expr::Not(Metadata::new(), y.clone())))
988 };
989 };
990 if let Expr::Atomic(_, Atom::Literal(Lit::Bool(y))) = y.as_ref() {
991 return if *y {
992 Ok(RuleEffect::pure(Moo::unwrap_or_clone(x.clone())))
994 } else {
995 Ok(RuleEffect::pure(Expr::Not(Metadata::new(), x.clone())))
997 };
998 };
999
1000 if x.identical_atom_to(y.as_ref())
1007 && (mode == PartialEvalMode::Local
1008 || (is_semantically_safe(x) && is_semantically_safe(y)))
1009 {
1010 return Ok(RuleEffect::pure(true.into()));
1011 }
1012
1013 Err(RuleNotApplicable)
1014 }
1015 Expr::Eq(_, x, y) => {
1016 if let Some((eq_result, _)) = simplify_reflexive_comparison_with_mode(x, y, mode) {
1017 Ok(RuleEffect::pure(Expr::Atomic(
1018 Metadata::new(),
1019 Atom::Literal(Lit::Bool(eq_result)),
1020 )))
1021 } else if let Expr::Atomic(_, Atom::Literal(lit)) = x.as_ref()
1022 && comparison_domain_lookup_is_cheap(y, mode)
1023 && let Some((Some(eq_result), _)) = simplify_comparison_with_literal(y, lit)
1024 {
1025 Ok(RuleEffect::pure(Expr::Atomic(
1026 Metadata::new(),
1027 Atom::Literal(Lit::Bool(eq_result)),
1028 )))
1029 } else if let Expr::Atomic(_, Atom::Literal(lit)) = y.as_ref()
1030 && comparison_domain_lookup_is_cheap(x, mode)
1031 && let Some((Some(eq_result), _)) = simplify_comparison_with_literal(x, lit)
1032 {
1033 Ok(RuleEffect::pure(Expr::Atomic(
1034 Metadata::new(),
1035 Atom::Literal(Lit::Bool(eq_result)),
1036 )))
1037 } else if let Some(atom) = try_lower_bool_atom_eq_true(expr) {
1038 Ok(RuleEffect::pure(atom))
1039 } else {
1040 Err(RuleNotApplicable)
1041 }
1042 }
1043 Expr::Neq(_, x, y) => {
1044 if let Some((_, neq_result)) = simplify_reflexive_comparison_with_mode(x, y, mode) {
1045 Ok(RuleEffect::pure(Expr::Atomic(
1046 Metadata::new(),
1047 Atom::Literal(Lit::Bool(neq_result)),
1048 )))
1049 } else if let Expr::Atomic(_, Atom::Literal(lit)) = x.as_ref()
1050 && comparison_domain_lookup_is_cheap(y, mode)
1051 && let Some((_, Some(neq_result))) = simplify_comparison_with_literal(y, lit)
1052 {
1053 Ok(RuleEffect::pure(Expr::Atomic(
1054 Metadata::new(),
1055 Atom::Literal(Lit::Bool(neq_result)),
1056 )))
1057 } else if let Expr::Atomic(_, Atom::Literal(lit)) = y.as_ref()
1058 && comparison_domain_lookup_is_cheap(x, mode)
1059 && let Some((_, Some(neq_result))) = simplify_comparison_with_literal(x, lit)
1060 {
1061 Ok(RuleEffect::pure(Expr::Atomic(
1062 Metadata::new(),
1063 Atom::Literal(Lit::Bool(neq_result)),
1064 )))
1065 } else {
1066 Err(RuleNotApplicable)
1067 }
1068 }
1069 Expr::Geq(_, _, _) => Err(RuleNotApplicable),
1070 Expr::Leq(_, _, _) => Err(RuleNotApplicable),
1071 Expr::Gt(_, _, _) => Err(RuleNotApplicable),
1072 Expr::Lt(_, _, _) => Err(RuleNotApplicable),
1073 Expr::SafeDiv(_, _, _) => Err(RuleNotApplicable),
1074 Expr::UnsafeDiv(_, _, _) => Err(RuleNotApplicable),
1075 Expr::Flatten(_, _, _) => Err(RuleNotApplicable), Expr::AllDiff(m, e) => {
1077 let Some((vec, _)) = Moo::unwrap_or_clone(e.clone()).unwrap_matrix_unchecked() else {
1078 return Err(RuleNotApplicable);
1079 };
1080
1081 let mut consts: HashSet<Lit> = HashSet::new();
1082
1083 for expr in vec {
1085 let Expr::Atomic(_, Atom::Literal(lit)) = expr else {
1086 return Err(RuleNotApplicable);
1087 };
1088 if !consts.insert(lit) {
1089 return Ok(RuleEffect::pure(Expr::Atomic(
1090 m.clone(),
1091 Atom::Literal(Lit::Bool(false)),
1092 )));
1093 }
1094 }
1095
1096 Ok(RuleEffect::pure(Expr::Atomic(
1097 m.clone(),
1098 Atom::Literal(Lit::Bool(true)),
1099 )))
1100 }
1101 Expr::Neg(_, _) => Err(RuleNotApplicable),
1102 Expr::Factorial(_, _) => Err(RuleNotApplicable),
1103 Expr::AuxDeclaration(_, _, _) => Err(RuleNotApplicable),
1104 Expr::UnsafeMod(_, _, _) => Err(RuleNotApplicable),
1105 Expr::SafeMod(_, _, _) => Err(RuleNotApplicable),
1106 Expr::UnsafePow(_, _, _) => Err(RuleNotApplicable),
1107 Expr::SafePow(_, base, exponent) => {
1108 let base = cheap_singleton_int_value(base).ok_or(RuleNotApplicable)?;
1109 let exponent = cheap_singleton_int_value(exponent).ok_or(RuleNotApplicable)?;
1110 if exponent < 0 || (base == 0 && exponent == 0) {
1111 return Err(RuleNotApplicable);
1112 }
1113 let value = base.checked_pow(exponent as u32).ok_or(RuleNotApplicable)?;
1114 Ok(RuleEffect::pure(Expr::from(value)))
1115 }
1116 Expr::Minus(_, _, _) => Err(RuleNotApplicable),
1117 Expr::Card(_, _) => Err(RuleNotApplicable),
1118
1119 Expr::FlatAllDiff(_, _) => Err(RuleNotApplicable),
1122 Expr::SmtDistinct(_, _) => Err(RuleNotApplicable),
1123 Expr::FlatAbsEq(_, _, _) => Err(RuleNotApplicable),
1124 Expr::FlatMinEq(_, _, _) => Err(RuleNotApplicable),
1125 Expr::FlatIneq(_, _, _, _) => Err(RuleNotApplicable),
1126 Expr::FlatMinusEq(_, _, _) => Err(RuleNotApplicable),
1127 Expr::FlatProductEq(_, _, _, _) => Err(RuleNotApplicable),
1128 Expr::FlatSumLeq(_, _, _) => Err(RuleNotApplicable),
1129 Expr::FlatSumGeq(_, _, _) => Err(RuleNotApplicable),
1130 Expr::FlatWatchedLiteral(_, _, _) => Err(RuleNotApplicable),
1131 Expr::FlatWeightedSumLeq(_, _, _, _) => Err(RuleNotApplicable),
1132 Expr::FlatWeightedSumGeq(_, _, _, _) => Err(RuleNotApplicable),
1133 Expr::MinionDivEqUndefZero(_, _, _, _) => Err(RuleNotApplicable),
1134 Expr::MinionModuloEqUndefZero(_, _, _, _) => Err(RuleNotApplicable),
1135 Expr::MinionPow(_, _, _, _) => Err(RuleNotApplicable),
1136 Expr::MinionReify(_, _, _) => Err(RuleNotApplicable),
1137 Expr::MinionReifyImply(_, _, _) => Err(RuleNotApplicable),
1138 Expr::MinionWInIntervalSet(_, _, _) => Err(RuleNotApplicable),
1139 Expr::MinionWInSet(_, _, _) => Err(RuleNotApplicable),
1140 Expr::MinionElementOne(_, _, _, _) => Err(RuleNotApplicable),
1141 Expr::SATInt(_, _, _, _) => Err(RuleNotApplicable),
1142 Expr::PairwiseSum(_, _, _) => Err(RuleNotApplicable),
1143 Expr::PairwiseProduct(_, _, _) => Err(RuleNotApplicable),
1144 Expr::Active(m, variant, alternative) => {
1145 let active_alternative = match variant.as_ref() {
1146 Expr::AbstractLiteral(_, AbstractLiteral::Variant(field)) => &field.name,
1147 Expr::Atomic(
1148 _,
1149 Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Variant(field))),
1150 ) => &field.name,
1151 _ => return Err(RuleNotApplicable),
1152 };
1153
1154 Ok(RuleEffect::pure(Expr::Atomic(
1155 m.clone(),
1156 Lit::Bool(active_alternative == alternative).into(),
1157 )))
1158 }
1159 Expr::Defined(_, _) => Err(RuleNotApplicable),
1166 Expr::Range(_, _) => Err(RuleNotApplicable),
1167 Expr::Image(_, _, _) => Err(RuleNotApplicable),
1168 Expr::ImageSet(_, _, _) => Err(RuleNotApplicable),
1169 Expr::PreImage(_, _, _) => Err(RuleNotApplicable),
1170 Expr::Inverse(_, _, _) => Err(RuleNotApplicable),
1171 Expr::PermInverse(_, _) => Err(RuleNotApplicable),
1172 Expr::Compose(_, _, _) => Err(RuleNotApplicable),
1173 Expr::Restrict(_, _, _) => Err(RuleNotApplicable),
1174 Expr::ToSet(_, _) => Err(RuleNotApplicable),
1175 Expr::ToMSet(_, _) => Err(RuleNotApplicable),
1176 Expr::ToRelation(_, _) => Err(RuleNotApplicable),
1177 Expr::RelationProj(_, _, _) => todo!(),
1178 Expr::Apart(_, _, _) => Err(RuleNotApplicable),
1179 Expr::Together(_, _, _) => Err(RuleNotApplicable),
1180 Expr::Participants(_, _) => Err(RuleNotApplicable),
1181 Expr::Party(_, _, _) => Err(RuleNotApplicable),
1182 Expr::Parts(_, _) => Err(RuleNotApplicable),
1183 Expr::Subsequence(_, _, _) => Err(RuleNotApplicable),
1184 Expr::Substring(_, _, _) => Err(RuleNotApplicable),
1185 Expr::LexLt(_, _, _) => Err(RuleNotApplicable),
1186 Expr::LexLeq(_, _, _) => Err(RuleNotApplicable),
1187 Expr::LexGt(_, _, _) => Err(RuleNotApplicable),
1188 Expr::LexGeq(_, _, _) => Err(RuleNotApplicable),
1189 Expr::FlatLexLt(_, _, _) => Err(RuleNotApplicable),
1190 Expr::FlatLexLeq(_, _, _) => Err(RuleNotApplicable),
1191 Expr::AllDifferentExcept(_, _, _) | Expr::ElementId(_, _, _) => Err(RuleNotApplicable),
1192 }
1193}
1194
1195fn is_empty_matrix_operand(operand: &Expr) -> bool {
1202 match operand {
1203 Expr::TypeAnnotation(_, inner, _) | Expr::DomainAnnotation(_, inner, _) => {
1204 is_empty_matrix_operand(inner)
1205 }
1206 Expr::AbstractLiteral(_, AbstractLiteral::Matrix(elems, _)) => elems.is_empty(),
1207 Expr::Atomic(_, Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Matrix(elems, _)))) => {
1208 elems.is_empty()
1209 }
1210 _ => false,
1211 }
1212}
1213
1214fn as_constant_bound_comparison(expr: &Expr) -> Option<(Atom, ConstantBoundOp, i32)> {
1216 as_constant_bound_comparison_ref(expr).map(|(lhs, op, rhs)| (lhs.clone(), op, rhs))
1217}
1218
1219fn as_constant_bound_comparison_ref(expr: &Expr) -> Option<(&Atom, ConstantBoundOp, i32)> {
1221 let (lhs, rhs, op) = match expr {
1222 Expr::Gt(_, lhs, rhs) => (lhs, rhs, ConstantBoundOp::Gt),
1223 Expr::Geq(_, lhs, rhs) => (lhs, rhs, ConstantBoundOp::Geq),
1224 Expr::Lt(_, lhs, rhs) => (lhs, rhs, ConstantBoundOp::Lt),
1225 Expr::Leq(_, lhs, rhs) => (lhs, rhs, ConstantBoundOp::Leq),
1226 _ => return None,
1227 };
1228
1229 let Expr::Atomic(_, Atom::Literal(Lit::Int(rhs_value))) = rhs.as_ref() else {
1230 return None;
1231 };
1232 let Expr::Atomic(_, lhs_atom) = lhs.as_ref() else {
1233 return None;
1234 };
1235 Some((lhs_atom, op, *rhs_value))
1236}
1237
1238fn make_constant_bound_comparison(lhs: Atom, op: ConstantBoundOp, rhs: i32) -> Expr {
1240 let lhs = Expr::Atomic(Metadata::new(), lhs);
1241 let rhs = Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Int(rhs)));
1242 match op {
1243 ConstantBoundOp::Gt => Expr::Gt(Metadata::new(), Moo::new(lhs), Moo::new(rhs)),
1244 ConstantBoundOp::Geq => Expr::Geq(Metadata::new(), Moo::new(lhs), Moo::new(rhs)),
1245 ConstantBoundOp::Lt => Expr::Lt(Metadata::new(), Moo::new(lhs), Moo::new(rhs)),
1246 ConstantBoundOp::Leq => Expr::Leq(Metadata::new(), Moo::new(lhs), Moo::new(rhs)),
1247 }
1248}
1249
1250fn merge_constant_bound(
1256 bounds: &mut IndexMap<(Atom, ConstantBoundOp), i32>,
1257 lhs: Atom,
1258 op: ConstantBoundOp,
1259 rhs: i32,
1260) {
1261 match bounds.entry((lhs, op)) {
1262 indexmap::map::Entry::Occupied(mut entry) => {
1263 let existing = entry.get_mut();
1264 if op.prefers_larger_rhs() {
1265 if rhs > *existing {
1266 *existing = rhs;
1267 }
1268 } else if rhs < *existing {
1269 *existing = rhs;
1270 }
1271 }
1272 indexmap::map::Entry::Vacant(entry) => {
1273 entry.insert(rhs);
1274 }
1275 }
1276}
1277
1278fn check_pairwise_or_tautologies(or_terms: &[Expr]) -> bool {
1288 #[allow(clippy::mutable_key_type)]
1291 let mut p_implies_q: HashSet<(&Atom, &Atom)> = HashSet::new();
1292 #[allow(clippy::mutable_key_type)]
1293 let mut p_implies_not_q: HashSet<(&Atom, &Atom)> = HashSet::new();
1294
1295 for term in or_terms {
1296 if let Expr::Imply(_, p, q) = term {
1297 if let Expr::Not(_, q_1) = q.as_ref() {
1298 if let (Expr::Atomic(_, p), Expr::Atomic(_, q)) = (p.as_ref(), q_1.as_ref()) {
1299 p_implies_not_q.insert((p, q));
1300 }
1301 } else if let (Expr::Atomic(_, p), Expr::Atomic(_, q)) = (p.as_ref(), q.as_ref()) {
1302 p_implies_q.insert((p, q));
1303 }
1304 }
1305 }
1306
1307 for &(p, q) in &p_implies_q {
1309 if p_implies_q.contains(&(q, p)) {
1310 return true;
1311 }
1312 }
1313
1314 p_implies_not_q
1316 .iter()
1317 .any(|pair| p_implies_q.contains(pair))
1318}
1319
1320#[cfg(test)]
1321mod tests {
1322 use super::*;
1323 use crate::ast::{DeclarationPtr, Domain, Name};
1324
1325 fn int_lit(value: i32) -> Expr {
1326 Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Int(value)))
1327 }
1328
1329 fn bool_lit(value: bool) -> Expr {
1330 Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Bool(value)))
1331 }
1332
1333 fn atom_ref(name: &str) -> Expr {
1334 Expr::Atomic(
1335 Metadata::new(),
1336 Atom::Reference(crate::ast::Reference::new(DeclarationPtr::new_find(
1337 Name::user(name),
1338 Domain::int(vec![Range::Bounded(1, 20)]),
1339 ))),
1340 )
1341 }
1342
1343 fn singleton_ref(name: &str, value: i32) -> Expr {
1344 Expr::Atomic(
1345 Metadata::new(),
1346 Atom::Reference(crate::ast::Reference::new(DeclarationPtr::new_find(
1347 Name::user(name),
1348 Domain::int(vec![Range::Bounded(value, value)]),
1349 ))),
1350 )
1351 }
1352
1353 fn safe_pow(base: Expr, exponent: Expr) -> Expr {
1354 Expr::SafePow(Metadata::new(), Moo::new(base), Moo::new(exponent))
1355 }
1356
1357 #[test]
1359 fn safe_pow_folds_singleton_operands() {
1360 let reduced = run_partial_evaluator_local(&safe_pow(singleton_ref("x", 2), int_lit(3)))
1361 .expect("evaluates")
1362 .new_expression;
1363 assert_eq!(reduced, int_lit(8));
1364 }
1365
1366 #[test]
1368 fn safe_pow_keeps_non_singleton_operands() {
1369 assert!(run_partial_evaluator_local(&safe_pow(atom_ref("x"), int_lit(3))).is_err());
1370 }
1371
1372 #[test]
1374 fn safe_pow_leaves_undefined_powers_alone() {
1375 assert!(run_partial_evaluator_local(&safe_pow(int_lit(0), int_lit(0))).is_err());
1376 assert!(run_partial_evaluator_local(&safe_pow(int_lit(2), int_lit(-1))).is_err());
1377 }
1378
1379 #[test]
1381 fn safe_pow_leaves_overflowing_powers_alone() {
1382 assert!(run_partial_evaluator_local(&safe_pow(int_lit(i32::MAX), int_lit(2))).is_err());
1383 }
1384
1385 fn and(exprs: Vec<Expr>) -> Expr {
1386 Expr::And(Metadata::new(), Moo::new(into_matrix_expr![exprs]))
1387 }
1388
1389 fn or(exprs: Vec<Expr>) -> Expr {
1390 Expr::Or(Metadata::new(), Moo::new(into_matrix_expr![exprs]))
1391 }
1392
1393 #[test]
1398 fn empty_and_is_true() {
1399 let reduced = run_partial_evaluator_local(&and(vec![]))
1400 .expect("evaluates")
1401 .new_expression;
1402 assert_eq!(reduced, bool_lit(true));
1403 }
1404
1405 #[test]
1407 fn empty_or_is_false() {
1408 let reduced = run_partial_evaluator_local(&or(vec![]))
1409 .expect("evaluates")
1410 .new_expression;
1411 assert_eq!(reduced, bool_lit(false));
1412 }
1413
1414 fn empty_matrix_with_empty_index_domain() -> Expr {
1418 Expr::AbstractLiteral(
1419 Metadata::new(),
1420 AbstractLiteral::Matrix(
1421 vec![],
1422 DomainPtr::from(Domain::int(vec![Range::Bounded(1, 0)])),
1423 ),
1424 )
1425 }
1426
1427 #[test]
1428 fn empty_and_with_empty_index_domain_is_true() {
1429 let expr = Expr::And(
1430 Metadata::new(),
1431 Moo::new(empty_matrix_with_empty_index_domain()),
1432 );
1433 let reduced = run_partial_evaluator_local(&expr)
1434 .expect("evaluates")
1435 .new_expression;
1436 assert_eq!(reduced, bool_lit(true));
1437 }
1438
1439 #[test]
1440 fn empty_or_with_empty_index_domain_is_false() {
1441 let expr = Expr::Or(
1442 Metadata::new(),
1443 Moo::new(empty_matrix_with_empty_index_domain()),
1444 );
1445 let reduced = run_partial_evaluator_local(&expr)
1446 .expect("evaluates")
1447 .new_expression;
1448 assert_eq!(reduced, bool_lit(false));
1449 }
1450
1451 #[test]
1452 fn non_empty_and_is_not_collapsed_to_a_boolean() {
1453 let x = atom_ref("x");
1454 let expr = and(vec![Expr::Gt(
1455 Metadata::new(),
1456 Moo::new(x),
1457 Moo::new(int_lit(3)),
1458 )]);
1459 if let Ok(reduced) = run_partial_evaluator_local(&expr) {
1461 assert_ne!(reduced.new_expression, bool_lit(true));
1462 assert_ne!(reduced.new_expression, bool_lit(false));
1463 }
1464 }
1465
1466 #[test]
1467 fn and_dominates_constant_lower_bounds_on_same_atom() {
1468 let x = atom_ref("x");
1469 let expr = and(vec![
1470 Expr::Gt(Metadata::new(), Moo::new(x.clone()), Moo::new(int_lit(3))),
1471 bool_lit(true),
1472 Expr::Gt(Metadata::new(), Moo::new(x.clone()), Moo::new(int_lit(9))),
1473 Expr::Gt(Metadata::new(), Moo::new(x), Moo::new(int_lit(1))),
1474 ]);
1475
1476 let reduced = run_partial_evaluator_local(&expr).unwrap().new_expression;
1477 let Expr::And(_, operands) = reduced else {
1478 panic!("expected And, got {reduced}");
1479 };
1480 let list = Moo::unwrap_or_clone(operands).unwrap_list().unwrap();
1481 assert_eq!(list.len(), 1);
1482 assert!(matches!(
1483 &list[0],
1484 Expr::Gt(_, lhs, rhs)
1485 if matches!(rhs.as_ref(), Expr::Atomic(_, Atom::Literal(Lit::Int(9))))
1486 && matches!(lhs.as_ref(), Expr::Atomic(_, Atom::Reference(_)))
1487 ));
1488 }
1489
1490 #[test]
1491 fn and_dominates_constant_upper_bounds_on_same_atom() {
1492 let x = atom_ref("x");
1493 let expr = and(vec![
1494 Expr::Leq(Metadata::new(), Moo::new(x.clone()), Moo::new(int_lit(8))),
1495 Expr::Leq(Metadata::new(), Moo::new(x), Moo::new(int_lit(4))),
1496 ]);
1497
1498 let reduced = run_partial_evaluator_local(&expr).unwrap().new_expression;
1499 let Expr::And(_, operands) = reduced else {
1500 panic!("expected And, got {reduced}");
1501 };
1502 let list = Moo::unwrap_or_clone(operands).unwrap_list().unwrap();
1503 assert_eq!(list.len(), 1);
1504 assert!(matches!(
1505 &list[0],
1506 Expr::Leq(_, _, rhs)
1507 if matches!(rhs.as_ref(), Expr::Atomic(_, Atom::Literal(Lit::Int(4))))
1508 ));
1509 }
1510
1511 #[test]
1512 fn and_does_not_merge_a_single_constant_bound() {
1513 let x = atom_ref("x");
1514 let expr = and(vec![Expr::Gt(
1515 Metadata::new(),
1516 Moo::new(x),
1517 Moo::new(int_lit(3)),
1518 )]);
1519 assert!(run_partial_evaluator_local(&expr).is_err());
1520 }
1521
1522 fn product(factors: Vec<Expr>) -> Expr {
1523 Expr::Product(Metadata::new(), Moo::new(into_matrix_expr![factors]))
1524 }
1525
1526 #[test]
1531 fn local_product_does_not_reshuffle_zero_factor() {
1532 let x = atom_ref("x");
1533 let constant_first = product(vec![int_lit(0), x.clone()]);
1534 let variable_first = product(vec![x, int_lit(0)]);
1535 assert!(run_partial_evaluator_local(&constant_first).is_err());
1536 assert!(run_partial_evaluator_local(&variable_first).is_err());
1537 }
1538
1539 #[test]
1541 fn deep_product_collapses_safe_zero_factor() {
1542 let expr = product(vec![int_lit(0), atom_ref("x")]);
1543 let reduced = run_partial_evaluator(&expr).unwrap().new_expression;
1544 assert_eq!(reduced, int_lit(0));
1545 }
1546
1547 #[test]
1548 fn symbolic_cardinality_is_left_for_representation_rules() {
1549 let set = Expr::Atomic(
1550 Metadata::new(),
1551 Atom::Reference(crate::ast::Reference::new(DeclarationPtr::new_find(
1552 Name::user("s"),
1553 Domain::set(
1554 crate::ast::SetAttr::new_max_size(2),
1555 Domain::int(vec![Range::Bounded(1, 2)]),
1556 ),
1557 ))),
1558 );
1559 let cardinality = Expr::Card(Metadata::new(), Moo::new(set));
1560
1561 assert!(run_partial_evaluator_local(&cardinality).is_err());
1562 }
1563}