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}