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, representation) => GroundDomain::Int(
42 Range::squeeze(
43 &ranges
44 .iter()
45 .map(|range| Range::new(range.low().copied(), range.high().copied()))
46 .collect::<Vec<_>>(),
47 ),
48 representation.clone(),
49 ),
50 _ => domain.clone(),
51 }
52}
53
54fn is_semantically_safe(expr: &Expr) -> bool {
56 fn helper(expr: &Expr, resolving: &mut HashSet<crate::ast::serde::ObjId>) -> bool {
57 if matches!(
58 expr,
59 Expr::UnsafeDiv(_, _, _)
60 | Expr::UnsafeMod(_, _, _)
61 | Expr::UnsafePow(_, _, _)
62 | Expr::UnsafeIndex(_, _, _)
63 | Expr::Bubble(_, _, _)
64 | Expr::UnsafeSlice(_, _, _)
65 | Expr::Pred(_, _)
66 | Expr::Succ(_, _)
67 ) {
68 return false;
69 }
70
71 if let Expr::Image(_, subject, argument) = expr
74 && crate::ast::expressions::image_can_be_undefined(subject, argument)
75 {
76 return false;
77 }
78
79 if let Expr::Atomic(_, Atom::Reference(reference)) = expr {
80 let id = reference.id();
81 if !resolving.insert(id.clone()) {
82 return false;
83 }
84 let is_safe = reference
85 .with_resolved_expression(|resolved| helper(resolved, resolving))
86 .unwrap_or(true);
87 resolving.remove(&id);
88 return is_safe;
89 }
90
91 let mut is_safe = true;
92 expr.for_each_expr_child(&mut |child| {
93 if is_safe {
94 is_safe = helper(child, resolving);
95 }
96 });
97 is_safe
98 }
99
100 helper(expr, &mut HashSet::new())
101}
102
103fn simplify_in_domain(expr: &Expr, domain: &DomainPtr) -> Option<bool> {
105 if !is_semantically_safe(expr) {
106 return None;
107 }
108
109 let expr_domain = resolved_ground_domain_of_for_partial_eval(expr)?;
110 let domain = domain.resolve().ok()?;
111 let intersection = expr_domain.intersect(&domain).ok()?;
112
113 if normalise_int_domain(&intersection) == normalise_int_domain(expr_domain.as_ref()) {
114 return Some(true);
115 }
116
117 if intersection.return_type() == ReturnType::Int && intersection.length().ok() == Some(0) {
118 return Some(false);
119 }
120
121 None
122}
123
124fn singleton_int_value(expr: &Expr) -> Option<i32> {
126 if let Ok(value) = expr.try_into() {
127 return Some(value);
128 }
129
130 let domain = resolved_ground_domain_of_for_partial_eval(expr)?;
131 let GroundDomain::Int(ranges, _) = domain.as_ref() else {
132 return None;
133 };
134 let [range] = ranges.as_slice() else {
135 return None;
136 };
137 let (Some(low), Some(high)) = (range.low(), range.high()) else {
138 return None;
139 };
140
141 if low == high { Some(*low) } else { None }
142}
143
144fn cheap_singleton_int_value(expr: &Expr) -> Option<i32> {
151 match expr {
152 Expr::Atomic(_, Atom::Literal(Lit::Int(value))) => Some(*value),
153 Expr::Atomic(_, Atom::Reference(reference)) => {
154 let domain = reference.domain()?.resolve().ok()?;
155 let GroundDomain::Int(ranges, _) = domain.as_ref() else {
156 return None;
157 };
158 let [range] = ranges.as_slice() else {
159 return None;
160 };
161 let (Some(low), Some(high)) = (range.low(), range.high()) else {
162 return None;
163 };
164 (low == high).then_some(*low)
165 }
166 _ => None,
167 }
168}
169
170fn matrix_index_offset(index_domain: &DomainPtr, index: i32) -> Option<usize> {
171 integer_index_offset(index_domain.as_int_ground()?, index)
172}
173
174fn ground_matrix_index_offset(index_domain: &GroundDomain, index: i32) -> Option<usize> {
175 let GroundDomain::Int(ranges, _) = index_domain else {
176 return None;
177 };
178 integer_index_offset(ranges, index)
179}
180
181fn integer_index_offset(ranges: &[Range<i32>], index: i32) -> Option<usize> {
183 let mut offset = 0usize;
184 for range in ranges {
185 let lower = i64::from(*range.low()?);
186 let index = i64::from(index);
187 let upper = range.high().map(|value| i64::from(*value));
188 if index >= lower && upper.is_none_or(|upper| index <= upper) {
189 return offset.checked_add(usize::try_from(index - lower).ok()?);
190 }
191 let length = (upper? - lower + 1).max(0);
192 offset = offset.checked_add(usize::try_from(length).ok()?)?;
193 }
194 None
195}
196
197fn resolve_matrix_element(subject: &Expr, index: i32) -> Option<Expr> {
202 match subject {
203 Expr::TypeAnnotation(_, inner, _) | Expr::DomainAnnotation(_, inner, _) => {
204 resolve_matrix_element(inner, index)
205 }
206 Expr::AbstractLiteral(_, AbstractLiteral::Matrix(elems, index_domain)) => elems
207 .get(matrix_index_offset(index_domain, index)?)
208 .cloned(),
209 Expr::Atomic(
210 _,
211 Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Matrix(elems, index_domain))),
212 ) => elems
213 .get(ground_matrix_index_offset(index_domain, index)?)
214 .cloned()
215 .map(|literal| Expr::Atomic(Metadata::new(), Atom::Literal(literal))),
216 Expr::Atomic(_, Atom::Reference(reference)) => reference
217 .with_resolved_expression(|resolved| resolve_matrix_element(resolved, index))
218 .flatten()
219 .or_else(|| {
220 let Lit::AbstractLiteral(AbstractLiteral::Matrix(elems, index_domain)) =
223 reference.resolve_constant()?
224 else {
225 return None;
226 };
227 elems
228 .get(ground_matrix_index_offset(&index_domain, index)?)
229 .cloned()
230 .map(|literal| Expr::Atomic(Metadata::new(), Atom::Literal(literal)))
231 }),
232 _ => None,
233 }
234}
235
236fn resolved_ground_domain_of_for_partial_eval(expr: &Expr) -> Option<Moo<GroundDomain>> {
238 match expr {
239 Expr::SafeIndex(_, subject, _) => {
240 let subject_domain = resolved_ground_domain_of_for_partial_eval(subject)?;
241 let GroundDomain::Matrix(elem_domain, _, _) = subject_domain.as_ref() else {
242 return None;
243 };
244
245 Some(elem_domain.clone())
246 }
247 Expr::SafeSlice(_, subject, indices) => {
248 let subject_domain = resolved_ground_domain_of_for_partial_eval(subject)?;
249 let GroundDomain::Matrix(elem_domain, index_domains, _) = subject_domain.as_ref()
250 else {
251 return None;
252 };
253 let sliced_dimension = indices.iter().position(Option::is_none);
254
255 match sliced_dimension {
256 Some(dimension) => Some(Moo::new(GroundDomain::Matrix(
257 elem_domain.clone(),
258 vec![index_domains[dimension].clone()],
259 None,
260 ))),
261 None => Some(elem_domain.clone()),
262 }
263 }
264 Expr::UnsafeIndex(_, _, _) | Expr::UnsafeSlice(_, _, _) => None,
265 _ => expr.domain_of()?.resolve().ok(),
266 }
267}
268
269fn is_empty_min_max(expr: &Expr) -> bool {
274 matches!(expr, Expr::Min(_, values) | Expr::Max(_, values) if is_empty_matrix_operand(values))
275}
276
277fn can_derive_unsafe_domain(expr: &Expr) -> bool {
280 matches!(
281 expr,
282 Expr::Image(_, _, _) | Expr::Pred(_, _) | Expr::Succ(_, _)
283 )
284}
285
286fn simplify_comparison_with_literal(
292 expr: &Expr,
293 lit: &Lit,
294) -> Option<(Option<bool>, Option<bool>)> {
295 let is_safe = is_semantically_safe(expr);
301 if !is_safe && !is_empty_min_max(expr) && !can_derive_unsafe_domain(expr) {
302 return None;
303 }
304
305 let expr_domain = resolved_ground_domain_of_for_partial_eval(expr)?;
306
307 if matches!(expr_domain.as_ref(), GroundDomain::Empty(_)) {
310 return Some((Some(false), Some(false)));
311 }
312
313 if !expr_domain.contains(lit).ok()? {
314 if let Expr::Atomic(_, Atom::Reference(reference)) = expr
318 && !reference.ptr().reprs().is_empty()
319 {
320 return None;
321 }
322 return Some((Some(false), is_safe.then_some(true)));
326 }
327
328 if !is_safe {
329 return None;
330 }
331
332 match (expr_domain.as_ref(), lit) {
333 (GroundDomain::Bool, Lit::Bool(_)) => None,
334 (GroundDomain::Int(ranges, _), Lit::Int(value)) => {
335 let [range] = ranges.as_slice() else {
336 return None;
337 };
338 let (Some(low), Some(high)) = (range.low(), range.high()) else {
339 return None;
340 };
341
342 if low == high && low == value {
343 Some((Some(true), Some(false)))
344 } else {
345 None
346 }
347 }
348 _ => None,
349 }
350}
351
352fn comparison_domain_lookup_is_cheap(expr: &Expr, mode: PartialEvalMode) -> bool {
358 mode == PartialEvalMode::Deep
359 || matches!(expr, Expr::Atomic(_, Atom::Reference(_)))
360 || is_empty_min_max(expr)
361}
362
363fn simplify_reflexive_comparison(x: &Expr, y: &Expr) -> Option<(bool, bool)> {
365 if x.identical_atom_to(y) && is_semantically_safe(x) && is_semantically_safe(y) {
366 return Some((true, false));
367 }
368
369 if is_semantically_safe(x) && is_semantically_safe(y) && x == y {
370 return Some((true, false));
371 }
372
373 None
374}
375
376fn simplify_reflexive_comparison_with_mode(
377 x: &Expr,
378 y: &Expr,
379 mode: PartialEvalMode,
380) -> Option<(bool, bool)> {
381 match mode {
382 PartialEvalMode::Deep => simplify_reflexive_comparison(x, y),
383 PartialEvalMode::Local => x.identical_atom_to(y).then_some((true, false)),
384 }
385}
386
387pub fn run_partial_evaluator(expr: &Expr) -> ApplicationResult {
388 run_partial_evaluator_with_mode(expr, PartialEvalMode::Deep)
389}
390
391pub fn run_partial_evaluator_local(expr: &Expr) -> ApplicationResult {
397 run_partial_evaluator_with_mode(expr, PartialEvalMode::Local)
398}
399
400#[derive(Clone, Copy, Debug, PartialEq, Eq)]
401enum PartialEvalMode {
402 Deep,
403 Local,
404}
405
406pub fn try_lower_bool_atom_eq_true(expr: &Expr) -> Option<Expr> {
415 let Expr::Eq(_, left, right) = expr else {
416 return None;
417 };
418
419 let atom = match (left.as_ref(), right.as_ref()) {
420 (Expr::Atomic(_, Atom::Literal(Lit::Bool(true))), Expr::Atomic(_, atom))
421 if !matches!(atom, Atom::Literal(_)) =>
422 {
423 right.as_ref()
424 }
425 (Expr::Atomic(_, atom), Expr::Atomic(_, Atom::Literal(Lit::Bool(true))))
426 if !matches!(atom, Atom::Literal(_)) =>
427 {
428 left.as_ref()
429 }
430 _ => return None,
431 };
432
433 if atom.return_type() != ReturnType::Bool {
434 return None;
435 }
436
437 Some(atom.clone())
438}
439
440fn run_partial_evaluator_with_mode(expr: &Expr, mode: PartialEvalMode) -> ApplicationResult {
441 match expr {
445 Expr::Difference(_, _, _) => Err(RuleNotApplicable),
446 Expr::Union(_, _, _) => Err(RuleNotApplicable),
447 Expr::In(_, _, _) => Err(RuleNotApplicable),
448 Expr::Intersect(_, _, _) => Err(RuleNotApplicable),
449 Expr::Supset(_, _, _) => Err(RuleNotApplicable),
450 Expr::SupsetEq(_, _, _) => Err(RuleNotApplicable),
451 Expr::Subset(_, _, _) => Err(RuleNotApplicable),
452 Expr::SubsetEq(_, _, _) => Err(RuleNotApplicable),
453 Expr::AbstractLiteral(_, _) => Err(RuleNotApplicable),
454 Expr::CatchUndef(_, _, _) => Err(RuleNotApplicable),
455 Expr::Comprehension(_, _) => Err(RuleNotApplicable),
456 Expr::DominanceRelation(_, _) => Err(RuleNotApplicable),
457 Expr::TypeAnnotation(_, _, _) => Err(RuleNotApplicable),
458 Expr::DomainAnnotation(_, _, _) => Err(RuleNotApplicable),
459 Expr::FromSolution(_, _) => Err(RuleNotApplicable),
460 Expr::Metavar(_, _) => Err(RuleNotApplicable),
461 Expr::UnsafeIndex(_, _, _) => Err(RuleNotApplicable),
462 Expr::UnsafeSlice(_, _, _) => Err(RuleNotApplicable),
463 Expr::Table(_, _, _) | Expr::FlatTable(..) | Expr::FlatShortTable(..) => {
464 Err(RuleNotApplicable)
465 }
466 Expr::NegativeTable(_, _, _) | Expr::ShortTable(_, _, _) => Err(RuleNotApplicable),
467 Expr::AtLeast(_, _, _, _) => Err(RuleNotApplicable),
468 Expr::AtMost(_, _, _, _) => Err(RuleNotApplicable),
469 Expr::Gcc(_, _, _, _) | Expr::GccWeak(_, _, _, _) => Err(RuleNotApplicable),
470 Expr::RecordField(_, _, _) => Err(RuleNotApplicable),
471 Expr::AttributeAsConstraint(_, _, _, _) => Err(RuleNotApplicable),
472 Expr::SafeIndex(_, subject, indices) => {
473 if indices.is_empty() {
475 return Err(RuleNotApplicable);
476 }
477
478 let index = singleton_int_value(&indices[0]).ok_or(RuleNotApplicable)?;
480
481 let selected = resolve_matrix_element(subject, index).ok_or(RuleNotApplicable)?;
482 if indices.len() == 1 {
483 Ok(RuleEffect::pure(selected))
484 } else {
485 Ok(RuleEffect::pure(Expr::SafeIndex(
486 Metadata::new(),
487 Moo::new(selected),
488 indices[1..].to_vec(),
489 )))
490 }
491 }
492 Expr::SafeSlice(_, _, _) => Err(RuleNotApplicable),
493 Expr::InDomain(_, x, domain) => {
494 if mode == PartialEvalMode::Deep
495 && let Some(result) = simplify_in_domain(x, domain)
496 {
497 Ok(RuleEffect::pure(Expr::Atomic(
498 Metadata::new(),
499 result.into(),
500 )))
501 } else if let Expr::Atomic(_, Atom::Reference(decl)) = x.as_ref() {
502 let decl_domain = decl
503 .domain()
504 .ok_or(RuleNotApplicable)?
505 .resolve()
506 .map_err(|_| RuleNotApplicable)?;
507 let domain = domain.resolve().map_err(|_| RuleNotApplicable)?;
508
509 let intersection = decl_domain
510 .intersect(&domain)
511 .map_err(|_| RuleNotApplicable)?;
512
513 if &intersection == decl_domain.as_ref() {
515 Ok(RuleEffect::pure(Expr::Atomic(Metadata::new(), true.into())))
516 }
517 else if intersection.return_type() == ReturnType::Int
525 && intersection.length().ok() == Some(0)
526 {
527 Ok(RuleEffect::pure(Expr::Atomic(
528 Metadata::new(),
529 false.into(),
530 )))
531 } else {
532 Err(RuleNotApplicable)
533 }
534 } else if let Expr::Atomic(_, Atom::Literal(lit)) = x.as_ref() {
535 if domain
536 .resolve()
537 .and_then(|gd| gd.contains(lit))
538 .map_err(|_| RuleNotApplicable)?
539 {
540 Ok(RuleEffect::pure(Expr::Atomic(Metadata::new(), true.into())))
541 } else {
542 Ok(RuleEffect::pure(Expr::Atomic(
543 Metadata::new(),
544 false.into(),
545 )))
546 }
547 } else {
548 Err(RuleNotApplicable)
549 }
550 }
551 Expr::Bubble(_, expr, cond) => {
552 if let Expr::Atomic(_, Atom::Literal(Lit::Bool(true))) = cond.as_ref() {
556 Ok(RuleEffect::pure(Moo::unwrap_or_clone(expr.clone())))
557 } else {
558 Err(RuleNotApplicable)
559 }
560 }
561 Expr::Atomic(_, _) => Err(RuleNotApplicable),
562 Expr::ToInt(_, expression) => {
563 if expression.return_type() == ReturnType::Int {
564 Ok(RuleEffect::pure(Moo::unwrap_or_clone(expression.clone())))
565 } else {
566 Err(RuleNotApplicable)
567 }
568 }
569 Expr::Abs(m, e) => match e.as_ref() {
570 Expr::Neg(_, inner) => Ok(RuleEffect::pure(Expr::Abs(m.clone(), inner.clone()))),
571 _ => Err(RuleNotApplicable),
572 },
573 Expr::Sum(m, vec) => {
574 let vec = vec.unwrap_list_cow().ok_or(RuleNotApplicable)?;
575 let mut acc = 0;
576 let mut n_consts = 0;
577 for expr in vec.iter() {
578 if let Expr::Atomic(_, Atom::Literal(Lit::Int(x))) = expr {
579 acc += *x;
580 n_consts += 1;
581 }
582 }
583
584 if n_consts <= 1 {
585 return Err(RuleNotApplicable);
586 }
587
588 let mut new_vec: Vec<Expr> = vec
589 .iter()
590 .filter(|expr| !matches!(expr, Expr::Atomic(_, Atom::Literal(Lit::Int(_)))))
591 .cloned()
592 .collect();
593 if acc != 0 {
594 new_vec.push(Expr::Atomic(
595 Default::default(),
596 Atom::Literal(Lit::Int(acc)),
597 ));
598 }
599
600 Ok(RuleEffect::pure(Expr::Sum(
601 m.clone(),
602 Moo::new(into_matrix_expr![new_vec]),
603 )))
604 }
605
606 Expr::Product(m, vec) => {
607 let mut acc = 1;
608 let mut n_consts = 0;
609 let vec = vec.unwrap_list_cow().ok_or(RuleNotApplicable)?;
610 for expr in vec.iter() {
611 if let Expr::Atomic(_, Atom::Literal(Lit::Int(x))) = expr {
612 acc *= *x;
613 n_consts += 1;
614 }
615 }
616
617 if n_consts == 0 {
618 return Err(RuleNotApplicable);
619 }
620
621 if acc == 0 && mode == PartialEvalMode::Local {
622 return Err(RuleNotApplicable);
623 }
624
625 if acc != 0 && n_consts == 1 {
626 return Err(RuleNotApplicable);
627 }
628
629 let mut new_vec: Vec<Expr> = vec
630 .iter()
631 .filter(|expr| !matches!(expr, Expr::Atomic(_, Atom::Literal(Lit::Int(_)))))
632 .cloned()
633 .collect();
634
635 new_vec.push(Expr::Atomic(
636 Default::default(),
637 Atom::Literal(Lit::Int(acc)),
638 ));
639 let new_product = Expr::Product(m.clone(), Moo::new(into_matrix_expr![new_vec]));
640
641 if acc == 0 {
642 if mode == PartialEvalMode::Deep && is_semantically_safe(&new_product) {
646 Ok(RuleEffect::pure(Expr::Atomic(
647 Default::default(),
648 Atom::Literal(Lit::Int(0)),
649 )))
650 } else {
651 Err(RuleNotApplicable)
652 }
653 } else {
654 Ok(RuleEffect::pure(new_product))
656 }
657 }
658
659 Expr::Min(m, e) => {
660 let Some(vec) = e.unwrap_list_cow() else {
661 return Err(RuleNotApplicable);
662 };
663 let mut acc: Option<i32> = None;
664 let mut n_consts = 0;
665 for expr in vec.iter() {
666 if let Expr::Atomic(_, Atom::Literal(Lit::Int(x))) = expr {
667 n_consts += 1;
668 acc = match acc {
669 Some(i) => {
670 if i > *x {
671 Some(*x)
672 } else {
673 Some(i)
674 }
675 }
676 None => Some(*x),
677 };
678 }
679 }
680
681 if n_consts <= 1 {
682 return Err(RuleNotApplicable);
683 }
684
685 let mut new_vec: Vec<Expr> = vec
686 .iter()
687 .filter(|expr| !matches!(expr, Expr::Atomic(_, Atom::Literal(Lit::Int(_)))))
688 .cloned()
689 .collect();
690 if let Some(i) = acc {
691 new_vec.push(Expr::Atomic(Default::default(), Atom::Literal(Lit::Int(i))));
692 }
693
694 Ok(RuleEffect::pure(Expr::Min(
695 m.clone(),
696 Moo::new(into_matrix_expr![new_vec]),
697 )))
698 }
699
700 Expr::Max(m, e) => {
701 let Some(vec) = e.unwrap_list_cow() else {
702 return Err(RuleNotApplicable);
703 };
704
705 let mut acc: Option<i32> = None;
706 let mut n_consts = 0;
707 for expr in vec.iter() {
708 if let Expr::Atomic(_, Atom::Literal(Lit::Int(x))) = expr {
709 n_consts += 1;
710 acc = match acc {
711 Some(i) => {
712 if i < *x {
713 Some(*x)
714 } else {
715 Some(i)
716 }
717 }
718 None => Some(*x),
719 };
720 }
721 }
722
723 if n_consts <= 1 {
724 return Err(RuleNotApplicable);
725 }
726
727 let mut new_vec: Vec<Expr> = vec
728 .iter()
729 .filter(|expr| !matches!(expr, Expr::Atomic(_, Atom::Literal(Lit::Int(_)))))
730 .cloned()
731 .collect();
732 if let Some(i) = acc {
733 new_vec.push(Expr::Atomic(Default::default(), Atom::Literal(Lit::Int(i))));
734 }
735
736 Ok(RuleEffect::pure(Expr::Max(
737 m.clone(),
738 Moo::new(into_matrix_expr![new_vec]),
739 )))
740 }
741 Expr::Not(_, e1) => {
742 let Expr::Imply(_, p, q) = e1.as_ref() else {
743 return Err(RuleNotApplicable);
744 };
745
746 if mode == PartialEvalMode::Deep && !is_semantically_safe(e1) {
747 return Err(RuleNotApplicable);
748 }
749
750 match (p.as_ref(), q.as_ref()) {
751 (_, Expr::Atomic(_, Atom::Literal(Lit::Bool(true)))) => {
752 Ok(RuleEffect::pure(Expr::from(false)))
753 }
754 (_, Expr::Atomic(_, Atom::Literal(Lit::Bool(false)))) => {
755 Ok(RuleEffect::pure(Moo::unwrap_or_clone(p.clone())))
756 }
757 (Expr::Atomic(_, Atom::Literal(Lit::Bool(true))), _) => {
758 Ok(RuleEffect::pure(Expr::Not(Metadata::new(), q.clone())))
759 }
760 (Expr::Atomic(_, Atom::Literal(Lit::Bool(false))), _) => {
761 Ok(RuleEffect::pure(Expr::from(false)))
762 }
763 _ => Err(RuleNotApplicable),
764 }
765 }
766 Expr::Or(m, e) => {
767 if is_empty_matrix_operand(e) {
769 return Ok(RuleEffect::pure(Expr::from(false)));
770 }
771
772 let Some(terms) = e.unwrap_list_cow() else {
773 return Err(RuleNotApplicable);
774 };
775
776 let mut has_changed = false;
777 let mut non_literal_terms = 0;
778
779 for expr in terms.iter() {
782 if let Expr::Atomic(_, Atom::Literal(Lit::Bool(x))) = expr {
783 has_changed = true;
784
785 if *x {
788 return Ok(RuleEffect::pure(true.into()));
789 }
790 } else {
791 non_literal_terms += 1;
792 }
793 }
794
795 if check_pairwise_or_tautologies(&terms) {
797 return Ok(RuleEffect::pure(true.into()));
798 }
799
800 if non_literal_terms == 0 {
802 return Ok(RuleEffect::pure(false.into()));
803 }
804
805 if !has_changed {
806 return Err(RuleNotApplicable);
807 }
808
809 let new_terms = terms
810 .iter()
811 .filter(|expr| !matches!(expr, Expr::Atomic(_, Atom::Literal(Lit::Bool(_)))))
812 .cloned()
813 .collect::<Vec<_>>();
814
815 Ok(RuleEffect::pure(Expr::Or(
816 m.clone(),
817 Moo::new(into_matrix_expr![new_terms]),
818 )))
819 }
820 Expr::And(_, e) => {
821 if is_empty_matrix_operand(e) {
828 return Ok(RuleEffect::pure(Expr::from(true)));
829 }
830
831 let Some(vec) = e.unwrap_list_cow() else {
832 return Err(RuleNotApplicable);
833 };
834
835 let mut has_changed: bool = false;
836 #[allow(clippy::mutable_key_type)]
839 let mut distinct_bounds: HashSet<(&Atom, ConstantBoundOp)> = HashSet::new();
840 let mut constant_bound_terms: usize = 0;
841 for expr in vec.iter() {
842 if let Expr::Atomic(_, Atom::Literal(Lit::Bool(x))) = expr {
843 if !x {
844 return Ok(RuleEffect::pure(Expr::Atomic(
845 Default::default(),
846 Atom::Literal(Lit::Bool(false)),
847 )));
848 }
849 has_changed = true;
850 } else if let Some((lhs, op, _)) = as_constant_bound_comparison_ref(expr) {
851 constant_bound_terms += 1;
852 distinct_bounds.insert((lhs, op));
853 }
854 }
855
856 if constant_bound_terms > distinct_bounds.len() {
858 has_changed = true;
859 }
860
861 if !has_changed {
862 return Err(RuleNotApplicable);
863 }
864 drop(distinct_bounds);
865
866 let mut new_vec: Vec<Expr> = Vec::new();
868 let mut constant_bounds: IndexMap<(Atom, ConstantBoundOp), i32> = IndexMap::new();
870 for expr in vec.iter() {
871 if matches!(expr, Expr::Atomic(_, Atom::Literal(Lit::Bool(_)))) {
872 continue;
873 } else if let Some((lhs, op, rhs)) = as_constant_bound_comparison(expr) {
874 merge_constant_bound(&mut constant_bounds, lhs, op, rhs);
875 } else {
876 new_vec.push(expr.clone());
877 }
878 }
879
880 for ((lhs, op), rhs) in constant_bounds {
881 new_vec.push(make_constant_bound_comparison(lhs, op, rhs));
882 }
883
884 if new_vec.is_empty() {
885 Ok(RuleEffect::pure(Expr::from(true)))
886 } else {
887 Ok(RuleEffect::pure(Expr::And(
888 Metadata::new(),
889 Moo::new(into_matrix_expr![new_vec]),
890 )))
891 }
892 }
893
894 Expr::Root(_, es) => {
896 match es.as_slice() {
897 [] => Err(RuleNotApplicable),
898 [Expr::And(_, _)] => Ok(()),
900 [_] => Err(RuleNotApplicable),
902 [_, _, ..] => Ok(()),
903 }?;
904
905 let mut new_vec: Vec<Expr> = Vec::new();
906 let mut has_changed: bool = false;
907 for expr in es {
908 match expr {
909 Expr::Atomic(_, Atom::Literal(Lit::Bool(x))) => {
910 has_changed = true;
911 if !x {
912 return Ok(RuleEffect::pure(Expr::Root(
914 Metadata::new(),
915 vec![Expr::Atomic(
916 Default::default(),
917 Atom::Literal(Lit::Bool(false)),
918 )],
919 )));
920 }
921 }
923
924 Expr::And(_, vecs) => match Moo::unwrap_or_clone(vecs.clone()).into_list() {
926 Some(list) => {
927 has_changed = true;
928 for conjunct in list {
929 match conjunct {
930 Expr::Atomic(_, Atom::Literal(Lit::Bool(false))) => {
931 return Ok(RuleEffect::pure(Expr::Root(
932 Metadata::new(),
933 vec![Expr::Atomic(
934 Default::default(),
935 Atom::Literal(Lit::Bool(false)),
936 )],
937 )));
938 }
939 Expr::Atomic(_, Atom::Literal(Lit::Bool(true))) => {}
940 other => new_vec.push(other),
941 }
942 }
943 }
944 None => new_vec.push(expr.clone()),
945 },
946 _ => new_vec.push(expr.clone()),
947 }
948 }
949
950 if !has_changed {
951 Err(RuleNotApplicable)
952 } else {
953 if new_vec.is_empty() {
954 new_vec.push(true.into());
955 }
956 Ok(RuleEffect::pure(Expr::Root(Metadata::new(), new_vec)))
957 }
958 }
959 Expr::Imply(_m, x, y) => {
960 if let Expr::Atomic(_, Atom::Literal(Lit::Bool(x))) = x.as_ref() {
961 return if *x {
962 Ok(RuleEffect::pure(Moo::unwrap_or_clone(y.clone())))
964 } else {
965 Ok(RuleEffect::pure(Expr::Atomic(Metadata::new(), true.into())))
967 };
968 };
969
970 if let Expr::Atomic(_, Atom::Literal(Lit::Bool(y))) = y.as_ref() {
971 return if *y {
972 Ok(RuleEffect::pure(Expr::from(true)))
974 } else {
975 Ok(RuleEffect::pure(Expr::Not(Metadata::new(), x.clone())))
977 };
978 };
979
980 if x.identical_atom_to(y.as_ref())
987 && (mode == PartialEvalMode::Local
988 || (is_semantically_safe(x) && is_semantically_safe(y)))
989 {
990 return Ok(RuleEffect::pure(true.into()));
991 }
992
993 Err(RuleNotApplicable)
994 }
995 Expr::Iff(_m, x, y) => {
996 if let Expr::Atomic(_, Atom::Literal(Lit::Bool(x))) = x.as_ref() {
997 return if *x {
998 Ok(RuleEffect::pure(Moo::unwrap_or_clone(y.clone())))
1000 } else {
1001 Ok(RuleEffect::pure(Expr::Not(Metadata::new(), y.clone())))
1003 };
1004 };
1005 if let Expr::Atomic(_, Atom::Literal(Lit::Bool(y))) = y.as_ref() {
1006 return if *y {
1007 Ok(RuleEffect::pure(Moo::unwrap_or_clone(x.clone())))
1009 } else {
1010 Ok(RuleEffect::pure(Expr::Not(Metadata::new(), x.clone())))
1012 };
1013 };
1014
1015 if x.identical_atom_to(y.as_ref())
1022 && (mode == PartialEvalMode::Local
1023 || (is_semantically_safe(x) && is_semantically_safe(y)))
1024 {
1025 return Ok(RuleEffect::pure(true.into()));
1026 }
1027
1028 Err(RuleNotApplicable)
1029 }
1030 Expr::Eq(_, x, y) => {
1031 if let Some((eq_result, _)) = simplify_reflexive_comparison_with_mode(x, y, mode) {
1032 Ok(RuleEffect::pure(Expr::Atomic(
1033 Metadata::new(),
1034 Atom::Literal(Lit::Bool(eq_result)),
1035 )))
1036 } else if let Expr::Atomic(_, Atom::Literal(lit)) = x.as_ref()
1037 && comparison_domain_lookup_is_cheap(y, mode)
1038 && let Some((Some(eq_result), _)) = simplify_comparison_with_literal(y, lit)
1039 {
1040 Ok(RuleEffect::pure(Expr::Atomic(
1041 Metadata::new(),
1042 Atom::Literal(Lit::Bool(eq_result)),
1043 )))
1044 } else if let Expr::Atomic(_, Atom::Literal(lit)) = y.as_ref()
1045 && comparison_domain_lookup_is_cheap(x, mode)
1046 && let Some((Some(eq_result), _)) = simplify_comparison_with_literal(x, lit)
1047 {
1048 Ok(RuleEffect::pure(Expr::Atomic(
1049 Metadata::new(),
1050 Atom::Literal(Lit::Bool(eq_result)),
1051 )))
1052 } else if let Some(atom) = try_lower_bool_atom_eq_true(expr) {
1053 Ok(RuleEffect::pure(atom))
1054 } else {
1055 Err(RuleNotApplicable)
1056 }
1057 }
1058 Expr::Neq(_, x, y) => {
1059 if let Some((_, neq_result)) = simplify_reflexive_comparison_with_mode(x, y, mode) {
1060 Ok(RuleEffect::pure(Expr::Atomic(
1061 Metadata::new(),
1062 Atom::Literal(Lit::Bool(neq_result)),
1063 )))
1064 } else if let Expr::Atomic(_, Atom::Literal(lit)) = x.as_ref()
1065 && comparison_domain_lookup_is_cheap(y, mode)
1066 && let Some((_, Some(neq_result))) = simplify_comparison_with_literal(y, lit)
1067 {
1068 Ok(RuleEffect::pure(Expr::Atomic(
1069 Metadata::new(),
1070 Atom::Literal(Lit::Bool(neq_result)),
1071 )))
1072 } else if let Expr::Atomic(_, Atom::Literal(lit)) = y.as_ref()
1073 && comparison_domain_lookup_is_cheap(x, mode)
1074 && let Some((_, Some(neq_result))) = simplify_comparison_with_literal(x, lit)
1075 {
1076 Ok(RuleEffect::pure(Expr::Atomic(
1077 Metadata::new(),
1078 Atom::Literal(Lit::Bool(neq_result)),
1079 )))
1080 } else {
1081 Err(RuleNotApplicable)
1082 }
1083 }
1084 Expr::Geq(_, _, _) => Err(RuleNotApplicable),
1085 Expr::Leq(_, _, _) => Err(RuleNotApplicable),
1086 Expr::Gt(_, _, _) => Err(RuleNotApplicable),
1087 Expr::Lt(_, _, _) => Err(RuleNotApplicable),
1088
1089 Expr::Pred(_, _) => Err(RuleNotApplicable),
1090 Expr::Succ(_, _) => Err(RuleNotApplicable),
1091
1092 Expr::SafeDiv(_, _, _) => Err(RuleNotApplicable),
1093 Expr::UnsafeDiv(_, _, _) => Err(RuleNotApplicable),
1094 Expr::Flatten(_, _, _) => Err(RuleNotApplicable), Expr::AllDiff(m, e) => {
1096 let Some((vec, _)) = Moo::unwrap_or_clone(e.clone()).unwrap_matrix_unchecked() else {
1097 return Err(RuleNotApplicable);
1098 };
1099
1100 let mut consts: HashSet<Lit> = HashSet::new();
1101
1102 for expr in vec {
1104 let Expr::Atomic(_, Atom::Literal(lit)) = expr else {
1105 return Err(RuleNotApplicable);
1106 };
1107 if !consts.insert(lit) {
1108 return Ok(RuleEffect::pure(Expr::Atomic(
1109 m.clone(),
1110 Atom::Literal(Lit::Bool(false)),
1111 )));
1112 }
1113 }
1114
1115 Ok(RuleEffect::pure(Expr::Atomic(
1116 m.clone(),
1117 Atom::Literal(Lit::Bool(true)),
1118 )))
1119 }
1120 Expr::Neg(_, _) => Err(RuleNotApplicable),
1121 Expr::Factorial(_, _) => Err(RuleNotApplicable),
1122 Expr::AuxDeclaration(_, _, _) => Err(RuleNotApplicable),
1123 Expr::UnsafeMod(_, _, _) => Err(RuleNotApplicable),
1124 Expr::SafeMod(_, _, _) => Err(RuleNotApplicable),
1125 Expr::UnsafePow(_, _, _) => Err(RuleNotApplicable),
1126 Expr::SafePow(_, base, exponent) => {
1127 let base = cheap_singleton_int_value(base).ok_or(RuleNotApplicable)?;
1128 let exponent = cheap_singleton_int_value(exponent).ok_or(RuleNotApplicable)?;
1129 if exponent < 0 || (base == 0 && exponent == 0) {
1130 return Err(RuleNotApplicable);
1131 }
1132 let value = base.checked_pow(exponent as u32).ok_or(RuleNotApplicable)?;
1133 Ok(RuleEffect::pure(Expr::from(value)))
1134 }
1135 Expr::Minus(_, _, _) => Err(RuleNotApplicable),
1136 Expr::Card(_, _) => Err(RuleNotApplicable),
1137
1138 Expr::FlatAllDiff(_, _) => Err(RuleNotApplicable),
1141 Expr::SmtDistinct(_, _) => Err(RuleNotApplicable),
1142 Expr::FlatAbsEq(_, _, _) => Err(RuleNotApplicable),
1143 Expr::FlatMinEq(_, _, _) => Err(RuleNotApplicable),
1144 Expr::FlatIneq(_, _, _, _) => Err(RuleNotApplicable),
1145 Expr::FlatMinusEq(_, _, _) => Err(RuleNotApplicable),
1146 Expr::FlatProductEq(_, _, _, _) => Err(RuleNotApplicable),
1147 Expr::FlatSumLeq(_, _, _) => Err(RuleNotApplicable),
1148 Expr::FlatSumGeq(_, _, _) => Err(RuleNotApplicable),
1149 Expr::FlatWatchedLiteral(_, _, _) => Err(RuleNotApplicable),
1150 Expr::FlatWeightedSumLeq(_, _, _, _) => Err(RuleNotApplicable),
1151 Expr::FlatWeightedSumGeq(_, _, _, _) => Err(RuleNotApplicable),
1152 Expr::MinionDivEqUndefZero(_, _, _, _) => Err(RuleNotApplicable),
1153 Expr::MinionModuloEqUndefZero(_, _, _, _) => Err(RuleNotApplicable),
1154 Expr::MinionPow(_, _, _, _) => Err(RuleNotApplicable),
1155 Expr::MinionReify(_, _, _) => Err(RuleNotApplicable),
1156 Expr::MinionReifyImply(_, _, _) => Err(RuleNotApplicable),
1157 Expr::MinionWInIntervalSet(_, _, _) => Err(RuleNotApplicable),
1158 Expr::MinionWInSet(_, _, _) => Err(RuleNotApplicable),
1159 Expr::MinionElementOne(_, _, _, _) => Err(RuleNotApplicable),
1160 Expr::SatObjective(..) | Expr::SATInt(_, _, _, _) => Err(RuleNotApplicable),
1161 Expr::PairwiseSum(_, _, _) => Err(RuleNotApplicable),
1162 Expr::PairwiseProduct(_, _, _) => Err(RuleNotApplicable),
1163 Expr::Active(m, variant, alternative) => {
1164 let active_alternative = match variant.as_ref() {
1165 Expr::AbstractLiteral(_, AbstractLiteral::Variant(field)) => &field.name,
1166 Expr::Atomic(
1167 _,
1168 Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Variant(field))),
1169 ) => &field.name,
1170 _ => return Err(RuleNotApplicable),
1171 };
1172
1173 Ok(RuleEffect::pure(Expr::Atomic(
1174 m.clone(),
1175 Lit::Bool(active_alternative == alternative).into(),
1176 )))
1177 }
1178 Expr::Defined(_, _) => Err(RuleNotApplicable),
1185 Expr::Range(_, _) => Err(RuleNotApplicable),
1186 Expr::Image(_, _, _) => Err(RuleNotApplicable),
1187 Expr::ImageSet(_, _, _) => Err(RuleNotApplicable),
1188 Expr::PreImage(_, _, _) => Err(RuleNotApplicable),
1189 Expr::Inverse(_, _, _) => Err(RuleNotApplicable),
1190 Expr::PermInverse(_, _) => Err(RuleNotApplicable),
1191 Expr::Compose(_, _, _) => Err(RuleNotApplicable),
1192 Expr::Restrict(_, _, _) => Err(RuleNotApplicable),
1193 Expr::ToSet(_, _) => Err(RuleNotApplicable),
1194 Expr::ToMSet(_, _) => Err(RuleNotApplicable),
1195 Expr::ToRelation(_, _) => Err(RuleNotApplicable),
1196 Expr::RelationProj(_, _, _) => todo!(),
1197 Expr::Apart(_, _, _) => Err(RuleNotApplicable),
1198 Expr::Together(_, _, _) => Err(RuleNotApplicable),
1199 Expr::Participants(_, _) => Err(RuleNotApplicable),
1200 Expr::Party(_, _, _) => Err(RuleNotApplicable),
1201 Expr::Parts(_, _) => Err(RuleNotApplicable),
1202 Expr::Subsequence(_, _, _) => Err(RuleNotApplicable),
1203 Expr::Substring(_, _, _) => Err(RuleNotApplicable),
1204 Expr::LexLt(_, _, _) => Err(RuleNotApplicable),
1205 Expr::LexLeq(_, _, _) => Err(RuleNotApplicable),
1206 Expr::LexGt(_, _, _) => Err(RuleNotApplicable),
1207 Expr::LexGeq(_, _, _) => Err(RuleNotApplicable),
1208 Expr::FlatLexLt(_, _, _) => Err(RuleNotApplicable),
1209 Expr::FlatLexLeq(_, _, _) => Err(RuleNotApplicable),
1210 Expr::AllDifferentExcept(_, _, _)
1211 | Expr::ElementId(_, _, _)
1212 | Expr::IndexOf(_, _, _)
1213 | Expr::SatElement(_, _, _, _)
1214 | Expr::SatAllDifferentComparisons(_, _) => Err(RuleNotApplicable),
1215 }
1216}
1217
1218fn is_empty_matrix_operand(operand: &Expr) -> bool {
1225 match operand {
1226 Expr::TypeAnnotation(_, inner, _) | Expr::DomainAnnotation(_, inner, _) => {
1227 is_empty_matrix_operand(inner)
1228 }
1229 Expr::AbstractLiteral(_, AbstractLiteral::Matrix(elems, _)) => elems.is_empty(),
1230 Expr::Atomic(_, Atom::Literal(Lit::AbstractLiteral(AbstractLiteral::Matrix(elems, _)))) => {
1231 elems.is_empty()
1232 }
1233 _ => false,
1234 }
1235}
1236
1237fn as_constant_bound_comparison(expr: &Expr) -> Option<(Atom, ConstantBoundOp, i32)> {
1239 as_constant_bound_comparison_ref(expr).map(|(lhs, op, rhs)| (lhs.clone(), op, rhs))
1240}
1241
1242fn as_constant_bound_comparison_ref(expr: &Expr) -> Option<(&Atom, ConstantBoundOp, i32)> {
1244 let (lhs, rhs, op) = match expr {
1245 Expr::Gt(_, lhs, rhs) => (lhs, rhs, ConstantBoundOp::Gt),
1246 Expr::Geq(_, lhs, rhs) => (lhs, rhs, ConstantBoundOp::Geq),
1247 Expr::Lt(_, lhs, rhs) => (lhs, rhs, ConstantBoundOp::Lt),
1248 Expr::Leq(_, lhs, rhs) => (lhs, rhs, ConstantBoundOp::Leq),
1249 _ => return None,
1250 };
1251
1252 let Expr::Atomic(_, Atom::Literal(Lit::Int(rhs_value))) = rhs.as_ref() else {
1253 return None;
1254 };
1255 let Expr::Atomic(_, lhs_atom) = lhs.as_ref() else {
1256 return None;
1257 };
1258 Some((lhs_atom, op, *rhs_value))
1259}
1260
1261fn make_constant_bound_comparison(lhs: Atom, op: ConstantBoundOp, rhs: i32) -> Expr {
1263 let lhs = Expr::Atomic(Metadata::new(), lhs);
1264 let rhs = Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Int(rhs)));
1265 match op {
1266 ConstantBoundOp::Gt => Expr::Gt(Metadata::new(), Moo::new(lhs), Moo::new(rhs)),
1267 ConstantBoundOp::Geq => Expr::Geq(Metadata::new(), Moo::new(lhs), Moo::new(rhs)),
1268 ConstantBoundOp::Lt => Expr::Lt(Metadata::new(), Moo::new(lhs), Moo::new(rhs)),
1269 ConstantBoundOp::Leq => Expr::Leq(Metadata::new(), Moo::new(lhs), Moo::new(rhs)),
1270 }
1271}
1272
1273fn merge_constant_bound(
1279 bounds: &mut IndexMap<(Atom, ConstantBoundOp), i32>,
1280 lhs: Atom,
1281 op: ConstantBoundOp,
1282 rhs: i32,
1283) {
1284 match bounds.entry((lhs, op)) {
1285 indexmap::map::Entry::Occupied(mut entry) => {
1286 let existing = entry.get_mut();
1287 if op.prefers_larger_rhs() {
1288 if rhs > *existing {
1289 *existing = rhs;
1290 }
1291 } else if rhs < *existing {
1292 *existing = rhs;
1293 }
1294 }
1295 indexmap::map::Entry::Vacant(entry) => {
1296 entry.insert(rhs);
1297 }
1298 }
1299}
1300
1301fn check_pairwise_or_tautologies(or_terms: &[Expr]) -> bool {
1311 #[allow(clippy::mutable_key_type)]
1314 let mut p_implies_q: HashSet<(&Atom, &Atom)> = HashSet::new();
1315 #[allow(clippy::mutable_key_type)]
1316 let mut p_implies_not_q: HashSet<(&Atom, &Atom)> = HashSet::new();
1317
1318 for term in or_terms {
1319 if let Expr::Imply(_, p, q) = term {
1320 if let Expr::Not(_, q_1) = q.as_ref() {
1321 if let (Expr::Atomic(_, p), Expr::Atomic(_, q)) = (p.as_ref(), q_1.as_ref()) {
1322 p_implies_not_q.insert((p, q));
1323 }
1324 } else if let (Expr::Atomic(_, p), Expr::Atomic(_, q)) = (p.as_ref(), q.as_ref()) {
1325 p_implies_q.insert((p, q));
1326 }
1327 }
1328 }
1329
1330 for &(p, q) in &p_implies_q {
1332 if p_implies_q.contains(&(q, p)) {
1333 return true;
1334 }
1335 }
1336
1337 p_implies_not_q
1339 .iter()
1340 .any(|pair| p_implies_q.contains(pair))
1341}
1342
1343#[cfg(test)]
1344mod tests {
1345 use super::*;
1346 use crate::ast::{DeclarationPtr, Domain, Name};
1347
1348 fn int_lit(value: i32) -> Expr {
1349 Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Int(value)))
1350 }
1351
1352 fn bool_lit(value: bool) -> Expr {
1353 Expr::Atomic(Metadata::new(), Atom::Literal(Lit::Bool(value)))
1354 }
1355
1356 fn atom_ref(name: &str) -> Expr {
1357 Expr::Atomic(
1358 Metadata::new(),
1359 Atom::Reference(crate::ast::Reference::new(DeclarationPtr::new_find(
1360 Name::user(name),
1361 Domain::int(vec![Range::Bounded(1, 20)]),
1362 ))),
1363 )
1364 }
1365
1366 fn singleton_ref(name: &str, value: i32) -> Expr {
1367 Expr::Atomic(
1368 Metadata::new(),
1369 Atom::Reference(crate::ast::Reference::new(DeclarationPtr::new_find(
1370 Name::user(name),
1371 Domain::int(vec![Range::Bounded(value, value)]),
1372 ))),
1373 )
1374 }
1375
1376 fn safe_pow(base: Expr, exponent: Expr) -> Expr {
1377 Expr::SafePow(Metadata::new(), Moo::new(base), Moo::new(exponent))
1378 }
1379
1380 #[test]
1381 fn in_domain_simplification_does_not_expand_large_integer_domains() {
1382 let domain = Domain::int(vec![Range::Bounded(-1_000_000_000, 1_000_000_000)]);
1383 let variable: Expr =
1384 crate::ast::Reference::new(DeclarationPtr::new_find(Name::user("wide"), domain)).into();
1385 for evaluate in [run_partial_evaluator, run_partial_evaluator_local] {
1386 for (target, expected) in [
1387 (Domain::int(vec![Range::Bounded(i32::MIN, i32::MAX)]), true),
1388 (
1389 Domain::int(vec![Range::Bounded(1_000_000_001, i32::MAX)]),
1390 false,
1391 ),
1392 ] {
1393 let expression =
1394 Expr::InDomain(Metadata::new(), Moo::new(variable.clone()), target);
1395 assert_eq!(
1396 evaluate(&expression).unwrap().new_expression,
1397 bool_lit(expected)
1398 );
1399 }
1400 let expression = Expr::InDomain(
1401 Metadata::new(),
1402 Moo::new(variable.clone()),
1403 Domain::int(vec![Range::Bounded(0, 1_000_000_000)]),
1404 );
1405 assert!(evaluate(&expression).is_err());
1406 }
1407 }
1408
1409 #[test]
1411 fn safe_pow_folds_singleton_operands() {
1412 let reduced = run_partial_evaluator_local(&safe_pow(singleton_ref("x", 2), int_lit(3)))
1413 .expect("evaluates")
1414 .new_expression;
1415 assert_eq!(reduced, int_lit(8));
1416 }
1417
1418 #[test]
1420 fn safe_pow_keeps_non_singleton_operands() {
1421 assert!(run_partial_evaluator_local(&safe_pow(atom_ref("x"), int_lit(3))).is_err());
1422 }
1423
1424 #[test]
1426 fn safe_pow_leaves_undefined_powers_alone() {
1427 assert!(run_partial_evaluator_local(&safe_pow(int_lit(0), int_lit(0))).is_err());
1428 assert!(run_partial_evaluator_local(&safe_pow(int_lit(2), int_lit(-1))).is_err());
1429 }
1430
1431 #[test]
1433 fn safe_pow_leaves_overflowing_powers_alone() {
1434 assert!(run_partial_evaluator_local(&safe_pow(int_lit(i32::MAX), int_lit(2))).is_err());
1435 }
1436
1437 fn and(exprs: Vec<Expr>) -> Expr {
1438 Expr::And(Metadata::new(), Moo::new(into_matrix_expr![exprs]))
1439 }
1440
1441 fn or(exprs: Vec<Expr>) -> Expr {
1442 Expr::Or(Metadata::new(), Moo::new(into_matrix_expr![exprs]))
1443 }
1444
1445 #[test]
1450 fn empty_and_is_true() {
1451 let reduced = run_partial_evaluator_local(&and(vec![]))
1452 .expect("evaluates")
1453 .new_expression;
1454 assert_eq!(reduced, bool_lit(true));
1455 }
1456
1457 #[test]
1459 fn empty_or_is_false() {
1460 let reduced = run_partial_evaluator_local(&or(vec![]))
1461 .expect("evaluates")
1462 .new_expression;
1463 assert_eq!(reduced, bool_lit(false));
1464 }
1465
1466 fn empty_matrix_with_empty_index_domain() -> Expr {
1470 Expr::AbstractLiteral(
1471 Metadata::new(),
1472 AbstractLiteral::Matrix(
1473 vec![],
1474 DomainPtr::from(Domain::int(vec![Range::Bounded(1, 0)])),
1475 ),
1476 )
1477 }
1478
1479 #[test]
1480 fn empty_and_with_empty_index_domain_is_true() {
1481 let expr = Expr::And(
1482 Metadata::new(),
1483 Moo::new(empty_matrix_with_empty_index_domain()),
1484 );
1485 let reduced = run_partial_evaluator_local(&expr)
1486 .expect("evaluates")
1487 .new_expression;
1488 assert_eq!(reduced, bool_lit(true));
1489 }
1490
1491 #[test]
1492 fn empty_or_with_empty_index_domain_is_false() {
1493 let expr = Expr::Or(
1494 Metadata::new(),
1495 Moo::new(empty_matrix_with_empty_index_domain()),
1496 );
1497 let reduced = run_partial_evaluator_local(&expr)
1498 .expect("evaluates")
1499 .new_expression;
1500 assert_eq!(reduced, bool_lit(false));
1501 }
1502
1503 #[test]
1504 fn non_empty_and_is_not_collapsed_to_a_boolean() {
1505 let x = atom_ref("x");
1506 let expr = and(vec![Expr::Gt(
1507 Metadata::new(),
1508 Moo::new(x),
1509 Moo::new(int_lit(3)),
1510 )]);
1511 if let Ok(reduced) = run_partial_evaluator_local(&expr) {
1513 assert_ne!(reduced.new_expression, bool_lit(true));
1514 assert_ne!(reduced.new_expression, bool_lit(false));
1515 }
1516 }
1517
1518 #[test]
1519 fn and_dominates_constant_lower_bounds_on_same_atom() {
1520 let x = atom_ref("x");
1521 let expr = and(vec![
1522 Expr::Gt(Metadata::new(), Moo::new(x.clone()), Moo::new(int_lit(3))),
1523 bool_lit(true),
1524 Expr::Gt(Metadata::new(), Moo::new(x.clone()), Moo::new(int_lit(9))),
1525 Expr::Gt(Metadata::new(), Moo::new(x), Moo::new(int_lit(1))),
1526 ]);
1527
1528 let reduced = run_partial_evaluator_local(&expr).unwrap().new_expression;
1529 let Expr::And(_, operands) = reduced else {
1530 panic!("expected And, got {reduced}");
1531 };
1532 let list = Moo::unwrap_or_clone(operands).unwrap_list().unwrap();
1533 assert_eq!(list.len(), 1);
1534 assert!(matches!(
1535 &list[0],
1536 Expr::Gt(_, lhs, rhs)
1537 if matches!(rhs.as_ref(), Expr::Atomic(_, Atom::Literal(Lit::Int(9))))
1538 && matches!(lhs.as_ref(), Expr::Atomic(_, Atom::Reference(_)))
1539 ));
1540 }
1541
1542 #[test]
1543 fn and_dominates_constant_upper_bounds_on_same_atom() {
1544 let x = atom_ref("x");
1545 let expr = and(vec![
1546 Expr::Leq(Metadata::new(), Moo::new(x.clone()), Moo::new(int_lit(8))),
1547 Expr::Leq(Metadata::new(), Moo::new(x), Moo::new(int_lit(4))),
1548 ]);
1549
1550 let reduced = run_partial_evaluator_local(&expr).unwrap().new_expression;
1551 let Expr::And(_, operands) = reduced else {
1552 panic!("expected And, got {reduced}");
1553 };
1554 let list = Moo::unwrap_or_clone(operands).unwrap_list().unwrap();
1555 assert_eq!(list.len(), 1);
1556 assert!(matches!(
1557 &list[0],
1558 Expr::Leq(_, _, rhs)
1559 if matches!(rhs.as_ref(), Expr::Atomic(_, Atom::Literal(Lit::Int(4))))
1560 ));
1561 }
1562
1563 #[test]
1564 fn and_does_not_merge_a_single_constant_bound() {
1565 let x = atom_ref("x");
1566 let expr = and(vec![Expr::Gt(
1567 Metadata::new(),
1568 Moo::new(x),
1569 Moo::new(int_lit(3)),
1570 )]);
1571 assert!(run_partial_evaluator_local(&expr).is_err());
1572 }
1573
1574 fn product(factors: Vec<Expr>) -> Expr {
1575 Expr::Product(Metadata::new(), Moo::new(into_matrix_expr![factors]))
1576 }
1577
1578 #[test]
1583 fn local_product_does_not_reshuffle_zero_factor() {
1584 let x = atom_ref("x");
1585 let constant_first = product(vec![int_lit(0), x.clone()]);
1586 let variable_first = product(vec![x, int_lit(0)]);
1587 assert!(run_partial_evaluator_local(&constant_first).is_err());
1588 assert!(run_partial_evaluator_local(&variable_first).is_err());
1589 }
1590
1591 #[test]
1593 fn deep_product_collapses_safe_zero_factor() {
1594 let expr = product(vec![int_lit(0), atom_ref("x")]);
1595 let reduced = run_partial_evaluator(&expr).unwrap().new_expression;
1596 assert_eq!(reduced, int_lit(0));
1597 }
1598
1599 #[test]
1600 fn symbolic_cardinality_is_left_for_representation_rules() {
1601 let set = Expr::Atomic(
1602 Metadata::new(),
1603 Atom::Reference(crate::ast::Reference::new(DeclarationPtr::new_find(
1604 Name::user("s"),
1605 Domain::set(
1606 crate::ast::SetAttr::new_max_size(2),
1607 Domain::int(vec![Range::Bounded(1, 2)]),
1608 ),
1609 ))),
1610 );
1611 let cardinality = Expr::Card(Metadata::new(), Moo::new(set));
1612
1613 assert!(run_partial_evaluator_local(&cardinality).is_err());
1614 }
1615}