conjure_core/ast/domains.rs
1#![warn(clippy::missing_errors_doc)]
2
3use std::{collections::BTreeSet, fmt::Display};
4
5use conjure_core::ast::SymbolTable;
6use itertools::Itertools;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use crate::ast::pretty::pretty_vec;
11use uniplate::{derive::Uniplate, Uniplate};
12
13use super::{records::RecordEntry, types::Typeable, AbstractLiteral, Literal, Name, ReturnType};
14
15#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum Range<A>
17where
18 A: Ord,
19{
20 Single(A),
21 Bounded(A, A),
22
23 /// int(i..)
24 UnboundedR(A),
25
26 /// int(..i)
27 UnboundedL(A),
28}
29
30impl<A: Ord> Range<A> {
31 pub fn contains(&self, val: &A) -> bool {
32 match self {
33 Range::Single(x) => x == val,
34 Range::Bounded(x, y) => x <= val && val <= y,
35 Range::UnboundedR(x) => x <= val,
36 Range::UnboundedL(x) => x >= val,
37 }
38 }
39}
40
41impl<A: Ord + Display> Display for Range<A> {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 Range::Single(i) => write!(f, "{i}"),
45 Range::Bounded(i, j) => write!(f, "{i}..{j}"),
46 Range::UnboundedR(i) => write!(f, "{i}.."),
47 Range::UnboundedL(i) => write!(f, "..{i}"),
48 }
49 }
50}
51
52#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Uniplate, Hash)]
53#[uniplate()]
54pub enum Domain {
55 Bool,
56
57 /// An integer domain.
58 ///
59 /// + If multiple ranges are inside the domain, the values in the domain are the union of these
60 /// ranges.
61 ///
62 /// + If no ranges are given, the int domain is considered unconstrained, and can take any
63 /// integer value.
64 Int(Vec<Range<i32>>),
65
66 /// An empty domain of the given type.
67 Empty(ReturnType),
68 Reference(Name),
69 Set(SetAttr, Box<Domain>),
70 /// A n-dimensional matrix with a value domain and n-index domains
71 Matrix(Box<Domain>, Vec<Domain>),
72 // A tuple of n domains (e.g. (int, bool))
73 Tuple(Vec<Domain>),
74
75 Record(Vec<RecordEntry>),
76}
77
78#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
79pub enum SetAttr {
80 None,
81 Size(i32),
82 MinSize(i32),
83 MaxSize(i32),
84 MinMaxSize(i32, i32),
85}
86impl Domain {
87 /// Returns true if `lit` is a member of the domain.
88 ///
89 /// # Errors
90 ///
91 /// - [`DomainOpError::InputContainsReference`] if the input domain is a reference or contains
92 /// a reference, meaning that its members cannot be determined.
93 pub fn contains(&self, lit: &Literal) -> Result<bool, DomainOpError> {
94 // not adding a generic wildcard condition for all domains, so that this gives a compile
95 // error when a domain is added.
96 match (self, lit) {
97 (Domain::Empty(_), _) => Ok(false),
98 (Domain::Int(ranges), Literal::Int(x)) => {
99 // unconstrained int domain
100 if ranges.is_empty() {
101 return Ok(true);
102 };
103
104 Ok(ranges.iter().any(|range| range.contains(x)))
105 }
106 (Domain::Int(_), _) => Ok(false),
107 (Domain::Bool, Literal::Bool(_)) => Ok(true),
108 (Domain::Bool, _) => Ok(false),
109 (Domain::Reference(_), _) => Err(DomainOpError::InputContainsReference),
110 (
111 Domain::Matrix(elem_domain, index_domains),
112 Literal::AbstractLiteral(AbstractLiteral::Matrix(elems, idx_domain)),
113 ) => {
114 let mut index_domains = index_domains.clone();
115 if index_domains
116 .pop()
117 .expect("a matrix should have atleast one index domain")
118 != **idx_domain
119 {
120 return Ok(false);
121 };
122
123 // matrix literals are represented as nested 1d matrices, so the elements of
124 // the matrix literal will be the inner dimensions of the matrix.
125 let next_elem_domain = if index_domains.is_empty() {
126 elem_domain.as_ref().clone()
127 } else {
128 Domain::Matrix(elem_domain.clone(), index_domains)
129 };
130
131 for elem in elems {
132 if !next_elem_domain.contains(elem)? {
133 return Ok(false);
134 }
135 }
136
137 Ok(true)
138 }
139 (
140 Domain::Tuple(elem_domains),
141 Literal::AbstractLiteral(AbstractLiteral::Tuple(literal_elems)),
142 ) => {
143 // for every element in the tuple literal, check if it is in the corresponding domain
144 for (elem_domain, elem) in itertools::izip!(elem_domains, literal_elems) {
145 if !elem_domain.contains(elem)? {
146 return Ok(false);
147 }
148 }
149
150 Ok(true)
151 }
152 (
153 Domain::Set(_, domain),
154 Literal::AbstractLiteral(AbstractLiteral::Set(literal_elems)),
155 ) => {
156 for elem in literal_elems {
157 if !domain.contains(elem)? {
158 return Ok(false);
159 }
160 }
161 Ok(true)
162 }
163 (
164 Domain::Record(entries),
165 Literal::AbstractLiteral(AbstractLiteral::Record(lit_entries)),
166 ) => {
167 for (entry, lit_entry) in itertools::izip!(entries, lit_entries) {
168 if entry.name != lit_entry.name || !(entry.domain.contains(&lit_entry.value)?) {
169 return Ok(false);
170 }
171 }
172 Ok(true)
173 }
174
175 (Domain::Record(_), _) => Ok(false),
176
177 (Domain::Matrix(_, _), _) => Ok(false),
178
179 (Domain::Set(_, _), _) => Ok(false),
180
181 (Domain::Tuple(_), _) => Ok(false),
182 }
183 }
184
185 /// Returns a list of all possible values in the domain.
186 ///
187 /// # Errors
188 ///
189 /// - [`DomainOpError::InputNotInteger`] if the domain is not an integer domain.
190 /// - [`DomainOpError::InputUnbounded`] if the domain is unbounded.
191 pub fn values_i32(&self) -> Result<Vec<i32>, DomainOpError> {
192 if let Domain::Empty(ReturnType::Int) = self {
193 return Ok(vec![]);
194 }
195 let Domain::Int(ranges) = self else {
196 return Err(DomainOpError::InputNotInteger(self.return_type().unwrap()));
197 };
198
199 if ranges.is_empty() {
200 return Err(DomainOpError::InputUnbounded);
201 }
202
203 let mut values = vec![];
204 for range in ranges {
205 match range {
206 Range::Single(i) => {
207 values.push(*i);
208 }
209 Range::Bounded(i, j) => {
210 values.extend(*i..=*j);
211 }
212 Range::UnboundedR(_) | Range::UnboundedL(_) => {
213 return Err(DomainOpError::InputUnbounded);
214 }
215 }
216 }
217
218 Ok(values)
219 }
220
221 /// Creates an [`Domain::Int`] containing the given integers.
222 ///
223 /// [`Domain::from_set_i32`] should be used instead where possible, as it is cheaper (it does
224 /// not need to sort its input).
225 ///
226 /// # Examples
227 ///
228 /// ```
229 /// use conjure_core::ast::{Domain,Range};
230 ///
231 /// let elements = vec![1,2,3,4,5];
232 ///
233 /// let domain = Domain::from_slice_i32(&elements);
234 ///
235 /// let Domain::Int(ranges) = domain else {
236 /// panic!("domain returned from from_slice_i32 should be a Domain::Int");
237 /// };
238 ///
239 /// assert_eq!(ranges,vec![Range::Bounded(1,5)]);
240 /// ```
241 ///
242 /// ```
243 /// use conjure_core::ast::{Domain,Range};
244 ///
245 /// let elements = vec![1,2,4,5,7,8,9,10];
246 ///
247 /// let domain = Domain::from_slice_i32(&elements);
248 ///
249 /// let Domain::Int(ranges) = domain else {
250 /// panic!("domain returned from from_slice_i32 should be a Domain::Int");
251 /// };
252 ///
253 /// assert_eq!(ranges,vec![Range::Bounded(1,2),Range::Bounded(4,5),Range::Bounded(7,10)]);
254 /// ```
255 ///
256 /// ```
257 /// use conjure_core::ast::{Domain,Range,ReturnType};
258 ///
259 /// let elements = vec![];
260 ///
261 /// let domain = Domain::from_slice_i32(&elements);
262 ///
263 /// assert!(matches!(domain,Domain::Empty(ReturnType::Int)))
264 /// ```
265 pub fn from_slice_i32(elements: &[i32]) -> Domain {
266 if elements.is_empty() {
267 return Domain::Empty(ReturnType::Int);
268 }
269
270 let set = BTreeSet::from_iter(elements.iter().cloned());
271
272 Domain::from_set_i32(&set)
273 }
274
275 /// Creates an [`Domain::Int`] containing the given integers.
276 ///
277 /// # Examples
278 ///
279 /// ```
280 /// use conjure_core::ast::{Domain,Range};
281 /// use std::collections::BTreeSet;
282 ///
283 /// let elements = BTreeSet::from([1,2,3,4,5]);
284 ///
285 /// let domain = Domain::from_set_i32(&elements);
286 ///
287 /// let Domain::Int(ranges) = domain else {
288 /// panic!("domain returned from from_slice_i32 should be a Domain::Int");
289 /// };
290 ///
291 /// assert_eq!(ranges,vec![Range::Bounded(1,5)]);
292 /// ```
293 ///
294 /// ```
295 /// use conjure_core::ast::{Domain,Range};
296 /// use std::collections::BTreeSet;
297 ///
298 /// let elements = BTreeSet::from([1,2,4,5,7,8,9,10]);
299 ///
300 /// let domain = Domain::from_set_i32(&elements);
301 ///
302 /// let Domain::Int(ranges) = domain else {
303 /// panic!("domain returned from from_set_i32 should be a Domain::Int");
304 /// };
305 ///
306 /// assert_eq!(ranges,vec![Range::Bounded(1,2),Range::Bounded(4,5),Range::Bounded(7,10)]);
307 /// ```
308 ///
309 /// ```
310 /// use conjure_core::ast::{Domain,Range,ReturnType};
311 /// use std::collections::BTreeSet;
312 ///
313 /// let elements = BTreeSet::from([]);
314 ///
315 /// let domain = Domain::from_set_i32(&elements);
316 ///
317 /// assert!(matches!(domain,Domain::Empty(ReturnType::Int)))
318 /// ```
319 pub fn from_set_i32(elements: &BTreeSet<i32>) -> Domain {
320 if elements.is_empty() {
321 return Domain::Empty(ReturnType::Int);
322 }
323 if elements.len() == 1 {
324 return Domain::Int(vec![Range::Single(*elements.first().unwrap())]);
325 }
326
327 let mut elems_iter = elements.iter().cloned();
328
329 let mut ranges: Vec<Range<i32>> = vec![];
330
331 // Loop over the elements in ascending order, turning all sequential runs of
332 // numbers into ranges.
333
334 // the bounds of the current run of numbers.
335 let mut lower = elems_iter
336 .next()
337 .expect("if we get here, elements should have => 2 elements");
338 let mut upper = lower;
339
340 for current in elems_iter {
341 // As elements is a BTreeSet, current is always strictly larger than lower.
342
343 if current == upper + 1 {
344 // current is part of the current run - we now have the run lower..current
345 //
346 upper = current;
347 } else {
348 // the run lower..upper has ended.
349 //
350 // Add the run lower..upper to the domain, and start a new run.
351
352 if lower == upper {
353 ranges.push(Range::Single(lower));
354 } else {
355 ranges.push(Range::Bounded(lower, upper));
356 }
357
358 lower = current;
359 upper = current;
360 }
361 }
362
363 // add the final run to the domain
364 if lower == upper {
365 ranges.push(Range::Single(lower));
366 } else {
367 ranges.push(Range::Bounded(lower, upper));
368 }
369
370 Domain::Int(ranges)
371 }
372
373 /// Gets all the [`Literal`] values inside this domain.
374 ///
375 /// # Errors
376 ///
377 /// - [`DomainOpError::InputNotInteger`] if the domain is not an integer domain.
378 /// - [`DomainOpError::InputContainsReference`] if the domain is a reference or contains a
379 /// reference, meaning that its values cannot be determined.
380 pub fn values(&self) -> Result<Vec<Literal>, DomainOpError> {
381 match self {
382 Domain::Empty(_) => Ok(vec![]),
383 Domain::Bool => Ok(vec![false.into(), true.into()]),
384 Domain::Int(_) => self
385 .values_i32()
386 .map(|xs| xs.iter().map(|x| Literal::Int(*x)).collect_vec()),
387
388 // ~niklasdewally: don't know how to define this for collections, so leaving it for
389 // now... However, it definitely can be done, as matrices can be indexed by matrices.
390 Domain::Set(_, _) => todo!(),
391 Domain::Matrix(_, _) => todo!(),
392 Domain::Reference(_) => Err(DomainOpError::InputContainsReference),
393 Domain::Tuple(_) => todo!(), // TODO: Can this be done?
394 Domain::Record(_) => todo!(),
395 }
396 }
397
398 /// Gets the length of this domain.
399 ///
400 /// # Errors
401 ///
402 /// - [`DomainOpError::InputUnbounded`] if the input domain is of infinite size.
403 /// - [`DomainOpError::InputContainsReference`] if the input domain is or contains a
404 /// domain reference, meaning that its size cannot be determined.
405 pub fn length(&self) -> Result<usize, DomainOpError> {
406 self.values().map(|x| x.len())
407 }
408
409 /// Returns the domain that is the result of applying a binary operation to two integer domains.
410 ///
411 /// The given operator may return `None` if the operation is not defined for its arguments.
412 /// Undefined values will not be included in the resulting domain.
413 ///
414 /// # Errors
415 ///
416 /// - [`DomainOpError::InputUnbounded`] if either of the input domains are unbounded.
417 /// - [`DomainOpError::InputNotInteger`] if either of the input domains are not integers.
418 pub fn apply_i32(
419 &self,
420 op: fn(i32, i32) -> Option<i32>,
421 other: &Domain,
422 ) -> Result<Domain, DomainOpError> {
423 let vs1 = self.values_i32()?;
424 let vs2 = other.values_i32()?;
425
426 let mut set = BTreeSet::new();
427 for (v1, v2) in itertools::iproduct!(vs1, vs2) {
428 if let Some(v) = op(v1, v2) {
429 set.insert(v);
430 }
431 }
432
433 Ok(Domain::from_set_i32(&set))
434 }
435 /// Returns true if the domain is finite.
436 ///
437 /// # Errors
438 ///
439 /// - [`DomainOpError::InputContainsReference`] if the input domain is or contains a
440 /// domain reference, meaning that its size cannot be determined.
441 pub fn is_finite(&self) -> Result<bool, DomainOpError> {
442 for domain in self.universe() {
443 if let Domain::Int(ranges) = domain {
444 if ranges.is_empty() {
445 return Ok(false);
446 }
447
448 if ranges
449 .iter()
450 .any(|range| matches!(range, Range::UnboundedL(_) | Range::UnboundedR(_)))
451 {
452 return Ok(false);
453 }
454 } else if let Domain::Reference(_) = domain {
455 return Err(DomainOpError::InputContainsReference);
456 }
457 }
458 Ok(true)
459 }
460
461 /// Resolves this domain to a ground domain, using the symbol table provided to resolve
462 /// references.
463 ///
464 /// A domain is ground iff it is not a domain reference, nor contains any domain references.
465 ///
466 /// See also: [`SymbolTable::resolve_domain`](crate::ast::SymbolTable::resolve_domain).
467 ///
468 /// # Panics
469 ///
470 /// + If a reference domain in `self` does not exist in the given symbol table.
471 pub fn resolve(mut self, symbols: &SymbolTable) -> Domain {
472 // FIXME: cannot use Uniplate::transform here due to reference lifetime shenanigans...
473 // dont see any reason why Uniplate::transform requires a closure that only uses borrows
474 // with a 'static lifetime... ~niklasdewally
475 // ..
476 // Also, still want to make the Uniplate variant which uses FnOnce not Fn with methods that
477 // take self instead of &self -- that would come in handy here!
478
479 let mut done_something = true;
480 while done_something {
481 done_something = false;
482 for (domain, ctx) in self.clone().contexts() {
483 if let Domain::Reference(name) = domain {
484 self = ctx(symbols
485 .resolve_domain(&name)
486 .expect("domain reference should exist in the symbol table")
487 .resolve(symbols));
488 done_something = true;
489 }
490 }
491 }
492 self
493 }
494
495 /// Calculates the intersection of two domains.
496 ///
497 /// # Errors
498 ///
499 /// - [`DomainOpError::InputUnbounded`] if either of the input domains are unbounded.
500 /// - [`DomainOpError::InputWrongType`] if the input domains are different types, or are not
501 /// integer or set domains.
502 pub fn intersect(&self, other: &Domain) -> Result<Domain, DomainOpError> {
503 // TODO: does not consider unbounded domains yet
504 // needs to be tested once comprehension rules are written
505
506 match (self, other) {
507 // one or more arguments is an empty int domain
508 (d @ Domain::Empty(ReturnType::Int), Domain::Int(_)) => Ok(d.clone()),
509 (Domain::Int(_), d @ Domain::Empty(ReturnType::Int)) => Ok(d.clone()),
510 (Domain::Empty(ReturnType::Int), d @ Domain::Empty(ReturnType::Int)) => Ok(d.clone()),
511
512 // one or more arguments is an empty set(int) domain
513 (Domain::Set(_, inner1), d @ Domain::Empty(ReturnType::Set(inner2)))
514 if matches!(**inner1, Domain::Int(_) | Domain::Empty(ReturnType::Int))
515 && matches!(**inner2, ReturnType::Int) =>
516 {
517 Ok(d.clone())
518 }
519 (d @ Domain::Empty(ReturnType::Set(inner1)), Domain::Set(_, inner2))
520 if matches!(**inner1, ReturnType::Int)
521 && matches!(**inner2, Domain::Int(_) | Domain::Empty(ReturnType::Int)) =>
522 {
523 Ok(d.clone())
524 }
525 (
526 d @ Domain::Empty(ReturnType::Set(inner1)),
527 Domain::Empty(ReturnType::Set(inner2)),
528 ) if matches!(**inner1, ReturnType::Int) && matches!(**inner2, ReturnType::Int) => {
529 Ok(d.clone())
530 }
531
532 // both arguments are non-empy
533 (Domain::Set(_, x), Domain::Set(_, y)) => {
534 Ok(Domain::Set(SetAttr::None, Box::new((*x).intersect(y)?)))
535 }
536
537 (Domain::Int(_), Domain::Int(_)) => {
538 let mut v: BTreeSet<i32> = BTreeSet::new();
539
540 let v1 = self.values_i32()?;
541 let v2 = other.values_i32()?;
542 for value1 in v1.iter() {
543 if v2.contains(value1) && !v.contains(value1) {
544 v.insert(*value1);
545 }
546 }
547 Ok(Domain::from_set_i32(&v))
548 }
549 _ => Err(DomainOpError::InputWrongType),
550 }
551 }
552
553 /// Calculates the union of two domains.
554 ///
555 /// # Errors
556 ///
557 /// - [`DomainOpError::InputUnbounded`] if either of the input domains are unbounded.
558 /// - [`DomainOpError::InputWrongType`] if the input domains are different types, or are not
559 /// integer or set domains.
560 pub fn union(&self, other: &Domain) -> Result<Domain, DomainOpError> {
561 // TODO: does not consider unbounded domains yet
562 // needs to be tested once comprehension rules are written
563 match (self, other) {
564 // one or more arguments is an empty integer domain
565 (Domain::Empty(ReturnType::Int), d @ Domain::Int(_)) => Ok(d.clone()),
566 (d @ Domain::Int(_), Domain::Empty(ReturnType::Int)) => Ok(d.clone()),
567 (Domain::Empty(ReturnType::Int), d @ Domain::Empty(ReturnType::Int)) => Ok(d.clone()),
568
569 // one or more arguments is an empty set(int) domain
570 (d @ Domain::Set(_, inner1), Domain::Empty(ReturnType::Set(inner2)))
571 if matches!(**inner1, Domain::Int(_) | Domain::Empty(ReturnType::Int))
572 && matches!(**inner2, ReturnType::Int) =>
573 {
574 Ok(d.clone())
575 }
576 (Domain::Empty(ReturnType::Set(inner1)), d @ Domain::Set(_, inner2))
577 if matches!(**inner1, ReturnType::Int)
578 && matches!(**inner2, Domain::Int(_) | Domain::Empty(ReturnType::Int)) =>
579 {
580 Ok(d.clone())
581 }
582 (
583 d @ Domain::Empty(ReturnType::Set(inner1)),
584 Domain::Empty(ReturnType::Set(inner2)),
585 ) if matches!(**inner1, ReturnType::Int) && matches!(**inner2, ReturnType::Int) => {
586 Ok(d.clone())
587 }
588
589 // both arguments are non empty
590 (Domain::Set(_, x), Domain::Set(_, y)) => {
591 Ok(Domain::Set(SetAttr::None, Box::new((*x).union(y)?)))
592 }
593 (Domain::Int(_), Domain::Int(_)) => {
594 let mut v: BTreeSet<i32> = BTreeSet::new();
595 let v1 = self.values_i32()?;
596 let v2 = other.values_i32()?;
597
598 for value1 in v1.iter() {
599 v.insert(*value1);
600 }
601
602 for value2 in v2.iter() {
603 v.insert(*value2);
604 }
605
606 Ok(Domain::from_set_i32(&v))
607 }
608 _ => Err(DomainOpError::InputWrongType),
609 }
610 }
611}
612
613impl Display for Domain {
614 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
615 match self {
616 Domain::Bool => {
617 write!(f, "bool")
618 }
619 Domain::Int(vec) => {
620 let domain_ranges: String = vec.iter().map(|x| format!("{x}")).join(",");
621
622 if domain_ranges.is_empty() {
623 write!(f, "int")
624 } else {
625 write!(f, "int({domain_ranges})")
626 }
627 }
628 Domain::Reference(name) => write!(f, "{}", name),
629 Domain::Set(_, domain) => {
630 write!(f, "set of ({})", domain)
631 }
632 Domain::Matrix(value_domain, index_domains) => {
633 write!(
634 f,
635 "matrix indexed by [{}] of {value_domain}",
636 pretty_vec(&index_domains.iter().collect_vec())
637 )
638 }
639 Domain::Tuple(domains) => {
640 write!(
641 f,
642 "tuple of ({})",
643 pretty_vec(&domains.iter().collect_vec())
644 )
645 }
646 Domain::Record(entries) => {
647 write!(
648 f,
649 "record of ({})",
650 pretty_vec(
651 &entries
652 .iter()
653 .map(|entry| format!("{}: {}", entry.name, entry.domain))
654 .collect_vec()
655 )
656 )
657 }
658 Domain::Empty(return_type) => write!(f, "empty({return_type:?}"),
659 }
660 }
661}
662
663impl Typeable for Domain {
664 fn return_type(&self) -> Option<ReturnType> {
665 todo!()
666 }
667}
668
669/// An error thrown by an operation on domains.
670#[non_exhaustive]
671#[derive(Clone, Debug, PartialEq, Eq, Error)]
672#[allow(clippy::enum_variant_names)] // all variant names start with Input at the moment, but that is ok.
673pub enum DomainOpError {
674 /// The operation only supports bounded / finite domains, but was given an unbounded input domain.
675 #[error("The operation only supports bounded / finite domains, but was given an unbounded input domain.")]
676 InputUnbounded,
677
678 /// The operation only supports integer input domains, but was given an input domain of a
679 /// different type.
680 #[error("The operation only supports integer input domains, but got a {0:?} input domain.")]
681 InputNotInteger(ReturnType),
682
683 /// The operation was given an input domain of the wrong type.
684 #[error("The operation was given input domains of the wrong type.")]
685 InputWrongType,
686
687 /// The operation failed as the input domain contained a reference.
688 #[error("The operation failed as the input domain contained a reference")]
689 InputContainsReference,
690}
691
692#[cfg(test)]
693mod tests {
694 use super::*;
695
696 #[test]
697 fn test_negative_product() {
698 let d1 = Domain::Int(vec![Range::Bounded(-2, 1)]);
699 let d2 = Domain::Int(vec![Range::Bounded(-2, 1)]);
700 let res = d1.apply_i32(|a, b| Some(a * b), &d2).unwrap();
701
702 assert!(matches!(res, Domain::Int(_)));
703 if let Domain::Int(ranges) = res {
704 assert!(!ranges.contains(&Range::Bounded(-4, 4)));
705 }
706 }
707
708 #[test]
709 fn test_negative_div() {
710 let d1 = Domain::Int(vec![Range::Bounded(-2, 1)]);
711 let d2 = Domain::Int(vec![Range::Bounded(-2, 1)]);
712 let res = d1
713 .apply_i32(|a, b| if b != 0 { Some(a / b) } else { None }, &d2)
714 .unwrap();
715
716 assert!(matches!(res, Domain::Int(_)));
717 if let Domain::Int(ranges) = res {
718 assert!(!ranges.contains(&Range::Bounded(-4, 4)));
719 }
720 }
721}