Skip to main content

taco_model_checker/
internal_spec.rs

1//! Internal specification types
2//!
3//! This module defines the specification format that model checkers in TACO
4//! operate on. An [`ErrorSpec`] describes the *erroneous* behavior of a threshold
5//! automaton, i.e., it is the negation of an ELTL property. A property is
6//! violated if there exists a run of the threshold automaton satisfying the
7//! error formula.
8
9use core::fmt;
10use core::ops::{Index, IndexMut};
11
12use log::{debug, warn};
13use taco_display_utils::join_iterator;
14use taco_threshold_automaton::ModifiableThresholdAutomaton;
15use taco_threshold_automaton::lia_threshold_automaton::LIAVariableConstraint;
16use taco_threshold_automaton::{
17    expressions::{
18        And, Atomic, BooleanConnective, BooleanExpression, Location, Or, Parameter, Variable,
19    },
20    impl_bitand,
21};
22
23use crate::internal_spec::extraction::ExtractionError;
24use crate::{ModelCheckerContext, SpecificationTrait, TASpecification};
25use crate::{
26    eltl::{ELTLExpression, remove_negations::NonNegatedELTLExpression},
27    internal_spec::upwards_closed_set::UpwardsClosedSet,
28};
29
30mod extraction;
31pub mod upwards_closed_set;
32
33/// Named specification of erroneous behavior
34///
35/// An [`ErrorSpec`] pairs the original ELTL property with the [`ErrorFormula`]
36/// describing its violations. The threshold automaton violates the property if
37/// and only if there exists a run satisfying the error formula.
38#[derive(Debug, Clone, PartialEq)]
39pub struct ErrorSpec {
40    /// Name of the property
41    name: String,
42    /// The ELTL property this error specification was derived from
43    source: Box<ELTLExpression>,
44    /// Formula describing the violations of the property
45    ef: ErrorFormula,
46}
47
48impl ErrorSpec {
49    /// Get the name of the specification
50    pub fn name(&self) -> &str {
51        &self.name
52    }
53
54    /// Get the ELTL property this specification was derived from
55    pub fn source(&self) -> &ELTLExpression {
56        &self.source
57    }
58
59    /// Get the formula describing the violations of the property
60    pub fn error_formula(&self) -> &ErrorFormula {
61        &self.ef
62    }
63
64    /// Create the threshold automata that need to be checked
65    pub fn create_tas_to_check<T: ModifiableThresholdAutomaton>(
66        &self,
67        ta: &T,
68    ) -> impl Iterator<Item = (ErrorTarget, T)> {
69        self.ef.iter().map(move |at| {
70            let mut ta = ta.clone();
71
72            ta.add_initial_location_constraints(
73                at.init_restriction().location_constraints().iter().cloned(),
74            );
75            ta.add_initial_variable_constraints(
76                at.init_restriction().variable_constraints().iter().cloned(),
77            );
78            ta.add_resilience_conditions(
79                at.init_restriction()
80                    .parameter_constraints()
81                    .iter()
82                    .cloned(),
83            );
84
85            (at.target.clone(), ta)
86        })
87    }
88}
89
90impl fmt::Display for ErrorSpec {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        write!(f, "{}: {}", self.name, self.ef)
93    }
94}
95
96impl<C: ModelCheckerContext> SpecificationTrait<C> for ErrorSpec {
97    type TransformationError = ExtractionError;
98    type InternalSpecType = ErrorTarget;
99
100    fn try_from_eltl(
101        spec: impl Iterator<Item = (String, ELTLExpression)>,
102        _ctx: &C,
103    ) -> Result<(Vec<Self>, Vec<String>), Self::TransformationError> {
104        let mut res_properties = Vec::new();
105        let mut untranslatable = Vec::new();
106
107        for (name, expr) in spec {
108            let res = ErrorSpec::from_named_eltl(name.clone(), expr.clone());
109
110            if matches!(&res, &Err(ExtractionError::NestedTemporalOperator))
111                || matches!(&res, &Err(ExtractionError::UnsupportedConjunction(_, _)))
112            {
113                warn!(
114                    "Specification '{name}' contains an operator nesting not yet supported by TACO. It will be reported as unknown."
115                );
116                debug!(
117                    "Classified property '{name}' as requiring operator nesting. Expression: {expr}"
118                );
119                untranslatable.push(name);
120                continue;
121            }
122
123            // Liveness properties that cannot be translated are reported as
124            // unknown, errors in safety properties abort the setup
125            if let Err(err) = &res
126                && contains_globally(&(!expr.clone()).into())
127            {
128                warn!(
129                    "Liveness specification '{name}' is not supported by TACO ({err}). It will be reported as unknown."
130                );
131                debug!("Failed to translate liveness property '{name}'. Expression: {expr}");
132                untranslatable.push(name);
133                continue;
134            }
135
136            let res = res?;
137            res_properties.push(res);
138        }
139
140        Ok((res_properties, untranslatable))
141    }
142
143    fn transform_threshold_automaton<
144        TA: taco_threshold_automaton::ThresholdAutomaton
145            + taco_threshold_automaton::ModifiableThresholdAutomaton,
146    >(
147        ta: TA,
148        specs: Vec<Self>,
149        _ctx: &C,
150    ) -> Vec<(String, Self::InternalSpecType, TA)> {
151        specs
152            .into_iter()
153            .flat_map(|spec| {
154                spec.create_tas_to_check(&ta)
155                    .map(|(target, ta)| {
156                        // A target without temporal part is violated if an
157                        // initial configuration satisfies the restriction
158                        let target = match target {
159                            ErrorTarget::Top => ErrorTarget::Reach(UpwardsClosedSet::new_top()),
160                            t => t,
161                        };
162                        (spec.name.clone(), target, ta)
163                    })
164                    .collect::<Vec<_>>()
165            })
166            .collect::<Vec<_>>()
167    }
168}
169
170/// Check whether the expression contains a `[]` operator
171fn contains_globally(expr: &NonNegatedELTLExpression) -> bool {
172    match expr {
173        NonNegatedELTLExpression::Globally(_) => true,
174        NonNegatedELTLExpression::Eventually(e) => contains_globally(e),
175        NonNegatedELTLExpression::And(l, r) | NonNegatedELTLExpression::Or(l, r) => {
176            contains_globally(l) || contains_globally(r)
177        }
178        _ => false,
179    }
180}
181
182/// Boolean combination of [`ErrorAtom`]s
183///
184/// A disjunction is satisfied if a run satisfying one of the disjuncts exists,
185/// therefore, the disjuncts can be checked in independent model checker runs.
186/// A conjunction requires a *single* run satisfying both conjuncts. Conjunctions
187/// for which a sound merge exists are already merged into a single atom during
188/// extraction (e.g., `[](a) && [](b)` becomes `[](a && b)`), the remaining
189/// conjunctions (e.g., `<>(a) && <>(b)`) need to be handled by the model
190/// checker.
191pub type ErrorFormula = Disjunction<ErrorAtom>;
192
193/// Specification that can be checked in a single model checker run
194///
195/// An atom is of the form `init && target`, where `init` restricts the initial
196/// configuration (and parameters) of the runs to consider and `target` is the
197/// temporal part of the specification.
198#[derive(Debug, Clone, PartialEq)]
199pub struct ErrorAtom {
200    /// Restriction on the initial configuration
201    init_restriction: InitRestriction,
202    /// Temporal part of the atom
203    target: ErrorTarget,
204}
205
206impl ErrorAtom {
207    /// Create a new atom
208    pub fn new(init_restriction: InitRestriction, target: ErrorTarget) -> Self {
209        Self {
210            init_restriction,
211            target,
212        }
213    }
214
215    /// Get the restriction on the initial configuration
216    pub fn init_restriction(&self) -> &InitRestriction {
217        &self.init_restriction
218    }
219
220    /// Get the temporal part of the atom
221    pub fn target(&self) -> &ErrorTarget {
222        &self.target
223    }
224
225    /// Check whether this atom is trivially satisfied by every run
226    pub fn is_top(&self) -> bool {
227        self.init_restriction.is_top() && matches!(self.target, ErrorTarget::Top)
228    }
229
230    /// Check whether this atom is unsatisfiable
231    pub fn is_bot(&self) -> bool {
232        self.target.is_bot()
233    }
234
235    /// The upwards closed sets appearing in the temporal part of this atom
236    fn targets(&self) -> impl Iterator<Item = &UpwardsClosedSet> {
237        self.target.sets()
238    }
239}
240
241impl TASpecification for ErrorAtom {
242    fn locs_appearing(&self) -> impl IntoIterator<Item = &Location> {
243        self.targets().flat_map(|set| set.locs_appearing())
244    }
245
246    fn var_constraint(&self) -> impl IntoIterator<Item = &LIAVariableConstraint> {
247        self.targets().flat_map(|set| set.var_constraint())
248    }
249}
250
251impl TASpecification for ErrorFormula {
252    /// Locations appearing in any of the disjuncts
253    ///
254    /// Used by the preprocessing to ensure that no location mentioned in the
255    /// specification is removed, so taking the union over all disjuncts is
256    /// sound.
257    fn locs_appearing(&self) -> impl IntoIterator<Item = &Location> {
258        self.iter().flat_map(|atom| atom.locs_appearing())
259    }
260
261    /// Variable constraints appearing in any of the disjuncts
262    ///
263    /// Used by the interval abstraction to compute the relevant bounds for the
264    /// shared variables, so taking the union over all disjuncts is sound.
265    fn var_constraint(&self) -> impl IntoIterator<Item = &LIAVariableConstraint> {
266        self.iter().flat_map(|atom| atom.var_constraint())
267    }
268}
269
270impl TASpecification for ErrorTarget {
271    /// Locations appearing in the temporal part of the target
272    fn locs_appearing(&self) -> impl IntoIterator<Item = &Location> {
273        self.sets().flat_map(|set| set.locs_appearing())
274    }
275
276    /// Variable constraints appearing in the temporal part of the target
277    fn var_constraint(&self) -> impl IntoIterator<Item = &LIAVariableConstraint> {
278        self.sets().flat_map(|set| set.var_constraint())
279    }
280}
281
282impl fmt::Display for ErrorAtom {
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        if !self.init_restriction.is_top() {
285            write!(f, "({}) && ", self.init_restriction)?;
286        }
287
288        write!(f, "({})", self.target)
289    }
290}
291
292/// Temporal part of an [`ErrorAtom`]
293#[derive(Debug, Clone, PartialEq)]
294pub enum ErrorTarget {
295    /// `<>(set)`: some configuration in the set is reachable
296    Reach(UpwardsClosedSet),
297    /// `[](set)`: there is an (infinite) run that never leaves the set
298    Invariant(UpwardsClosedSet),
299    /// `<>(pre && [](inv))`: there is an (infinite) run that reaches a
300    /// configuration in `pre` and never leaves `inv` afterwards
301    Ensure {
302        /// Set of configurations to reach
303        pre: UpwardsClosedSet,
304        /// Set of configurations that is never left after reaching `pre`
305        inv: UpwardsClosedSet,
306    },
307    /// `[](pre && <>(inv))`: there is an (infinite) run that never leaves
308    /// `pre` and visits `inv` infinitely often
309    Repeat {
310        /// Set of configurations that is never left
311        pre: UpwardsClosedSet,
312        /// Set of configurations that is visited infinitely often
313        inv: UpwardsClosedSet,
314    },
315    /// No temporal part: the atom is only a restriction on the initial
316    /// configuration
317    Top,
318}
319
320impl ErrorTarget {
321    /// Check whether the target is unsatisfiable
322    pub fn is_bot(&self) -> bool {
323        match self {
324            ErrorTarget::Reach(set) | ErrorTarget::Invariant(set) => set.is_bot(),
325            ErrorTarget::Ensure { pre, inv } | ErrorTarget::Repeat { pre, inv } => {
326                pre.is_bot() || inv.is_bot()
327            }
328            ErrorTarget::Top => false,
329        }
330    }
331
332    /// Check whether the target is always satisfiable
333    pub fn is_top(&self) -> bool {
334        match self {
335            ErrorTarget::Reach(s) | ErrorTarget::Invariant(s) => s.is_top(),
336            ErrorTarget::Ensure { pre, inv } | ErrorTarget::Repeat { pre, inv } => {
337                pre.is_top() && inv.is_top()
338            }
339            ErrorTarget::Top => true,
340        }
341    }
342
343    /// Create a new atom requiring to reach an upwards closed set of locations
344    pub fn new_reach(tgt: UpwardsClosedSet) -> Self {
345        Self::Reach(tgt)
346    }
347
348    /// Create a new atom requiring to never leave an upwards closed set of
349    /// locations
350    pub fn new_invariant(tgt: UpwardsClosedSet) -> Self {
351        Self::Invariant(tgt)
352    }
353
354    /// Create a new atom requiring to reach a configuration in `pre` while
355    /// never leaving `inv` afterwards
356    pub fn new_ensure(pre: UpwardsClosedSet, inv: UpwardsClosedSet) -> Self {
357        Self::Ensure { pre, inv }
358    }
359
360    /// Create a new atom requiring to ensure that a run never leaves `pre`
361    /// while eventually reaching `inv`
362    pub fn new_repeat(pre: UpwardsClosedSet, inv: UpwardsClosedSet) -> Self {
363        Self::Repeat { pre, inv }
364    }
365
366    /// Check whether specification contains a reachability constraint where at
367    /// least one location needs to be empty
368    ///
369    /// Only [`ErrorTarget::Reach`] targets are considered, the other temporal
370    /// targets always return `false`.
371    pub fn contains_reachability_constraint(&self) -> bool {
372        match self {
373            ErrorTarget::Reach(s) => s.contains_reachability_constraint(),
374            _ => false,
375        }
376    }
377
378    /// The upwards closed sets appearing in this target
379    fn sets(&self) -> impl Iterator<Item = &UpwardsClosedSet> {
380        let (pre, inv) = match self {
381            ErrorTarget::Reach(set) | ErrorTarget::Invariant(set) => (Some(set), None),
382            ErrorTarget::Ensure { pre, inv } | ErrorTarget::Repeat { pre, inv } => {
383                (Some(pre), Some(inv))
384            }
385            ErrorTarget::Top => (None, None),
386        };
387
388        [pre, inv].into_iter().flatten()
389    }
390}
391
392impl fmt::Display for ErrorTarget {
393    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
394        match self {
395            ErrorTarget::Reach(set) => write!(f, "<>({set})"),
396            ErrorTarget::Invariant(set) => write!(f, "[]({set})"),
397            ErrorTarget::Ensure { pre, inv } => write!(f, "<>(({pre}) && []({inv}))"),
398            ErrorTarget::Repeat { pre, inv } => write!(f, "[](({pre}) && <>({inv}))"),
399            ErrorTarget::Top => write!(f, "true"),
400        }
401    }
402}
403
404/// Restriction on the initial configuration of a run
405///
406/// The restriction is a conjunction of arbitrary boolean constraints, split by
407/// the kind of atoms they range over. Constraints on parameters are rigid,
408/// i.e., they hold at every position of a run.
409#[derive(Debug, Clone, PartialEq, Default)]
410pub struct InitRestriction {
411    /// Conjunction of conditions on the parameters
412    precondition_par: Conjunction<BooleanExpression<Parameter>>,
413    /// Conjunction of conditions on the initial distribution of processes in
414    /// the threshold automaton
415    precondition_loc: Conjunction<BooleanExpression<Location>>,
416    /// Conjunction of conditions on the initial valuation of the shared
417    /// variables
418    precondition_var: Conjunction<BooleanExpression<Variable>>,
419}
420
421impl InitRestriction {
422    /// Create a restriction that allows every initial configuration
423    pub fn new_top() -> Self {
424        Self::default()
425    }
426
427    /// Create a restriction consisting of a single parameter constraint
428    pub fn new_parameter_constraint(constr: BooleanExpression<Parameter>) -> Self {
429        Self {
430            precondition_par: [constr].into(),
431            ..Default::default()
432        }
433    }
434
435    /// Create a restriction consisting of a single constraint on the initial
436    /// distribution of processes
437    pub fn new_location_constraint(constr: BooleanExpression<Location>) -> Self {
438        Self {
439            precondition_loc: [constr].into(),
440            ..Default::default()
441        }
442    }
443
444    /// Create a restriction consisting of a single constraint on the initial
445    /// valuation of the shared variables
446    pub fn new_variable_constraint(constr: BooleanExpression<Variable>) -> Self {
447        Self {
448            precondition_var: [constr].into(),
449            ..Default::default()
450        }
451    }
452
453    /// Check whether the restriction allows every initial configuration
454    pub fn is_top(&self) -> bool {
455        self.precondition_par.is_empty()
456            && self.precondition_loc.is_empty()
457            && self.precondition_var.is_empty()
458    }
459
460    /// Disjunction of two restrictions, if a sound merge exists
461    ///
462    /// The restriction is a conjunction of one condition per kind of atom.
463    /// Because of this, the disjunction is only representable if both
464    /// restrictions differ in at most one kind:
465    /// `(p && l1 && v) || (p && l2 && v)` is equal to `p && (l1 || l2) && v`.
466    pub fn try_or(&self, other: &Self) -> Option<Self> {
467        let differing = [
468            self.precondition_par != other.precondition_par,
469            self.precondition_loc != other.precondition_loc,
470            self.precondition_var != other.precondition_var,
471        ]
472        .into_iter()
473        .filter(|differs| *differs)
474        .count();
475
476        if differing > 1 {
477            return None;
478        }
479
480        // Equal kinds are unchanged by `or_conjunctions`
481        Some(Self {
482            precondition_par: or_conjunctions(
483                self.precondition_par.clone(),
484                other.precondition_par.clone(),
485            ),
486            precondition_loc: or_conjunctions(
487                self.precondition_loc.clone(),
488                other.precondition_loc.clone(),
489            ),
490            precondition_var: or_conjunctions(
491                self.precondition_var.clone(),
492                other.precondition_var.clone(),
493            ),
494        })
495    }
496
497    /// Conditions on the parameters
498    pub fn parameter_constraints(&self) -> &Conjunction<BooleanExpression<Parameter>> {
499        &self.precondition_par
500    }
501
502    /// Conditions on the initial distribution of processes
503    pub fn location_constraints(&self) -> &Conjunction<BooleanExpression<Location>> {
504        &self.precondition_loc
505    }
506
507    /// Conditions on the initial valuation of the shared variables
508    pub fn variable_constraints(&self) -> &Conjunction<BooleanExpression<Variable>> {
509        &self.precondition_var
510    }
511}
512
513/// Build the disjunction of two conjunctions of boolean expressions as a single
514/// boolean expression
515fn or_conjunctions<T: Atomic>(
516    lhs: Conjunction<BooleanExpression<T>>,
517    rhs: Conjunction<BooleanExpression<T>>,
518) -> Conjunction<BooleanExpression<T>> {
519    // `x || x` is `x`
520    if lhs == rhs {
521        return lhs;
522    }
523
524    // `true || x` is `true`
525    if lhs.is_empty() || rhs.is_empty() {
526        return Conjunction::default();
527    }
528
529    let fold = |conj: Conjunction<BooleanExpression<T>>| {
530        conj.into_iter()
531            .reduce(|acc, c| {
532                BooleanExpression::BinaryExpression(
533                    Box::new(acc),
534                    BooleanConnective::And,
535                    Box::new(c),
536                )
537            })
538            .expect("conjunction is not empty")
539    };
540
541    [BooleanExpression::BinaryExpression(
542        Box::new(fold(lhs)),
543        BooleanConnective::Or,
544        Box::new(fold(rhs)),
545    )]
546    .into()
547}
548
549impl And for InitRestriction {
550    fn and(self, other: Self) -> Self {
551        Self {
552            precondition_par: self.precondition_par.and(other.precondition_par),
553            precondition_loc: self.precondition_loc.and(other.precondition_loc),
554            precondition_var: self.precondition_var.and(other.precondition_var),
555        }
556    }
557}
558impl_bitand!(InitRestriction);
559
560impl fmt::Display for InitRestriction {
561    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562        if self.is_top() {
563            return write!(f, "true");
564        }
565
566        let parts = [
567            self.precondition_par.to_string(),
568            self.precondition_loc.to_string(),
569            self.precondition_var.to_string(),
570        ];
571
572        write!(
573            f,
574            "{}",
575            join_iterator(parts.iter().filter(|p| !p.is_empty()), " && ")
576        )
577    }
578}
579
580/// Disjunction of `T`.
581#[derive(Debug, Clone)]
582pub struct Disjunction<T: PartialEq>(Vec<T>);
583
584/// Conjunction of `T`
585#[derive(Debug, Clone)]
586pub struct Conjunction<T: PartialEq>(Vec<T>);
587
588/// Remove duplicate elements from a vector in place
589fn dedup<T: PartialEq>(vec: &mut Vec<T>) {
590    let mut write = 0;
591    for read in 0..vec.len() {
592        // Keep the element only if it does not already occur in the kept prefix
593        if !vec[..write].contains(&vec[read]) {
594            vec.swap(write, read);
595            write += 1;
596        }
597    }
598    vec.truncate(write);
599}
600
601impl<T: PartialEq> Disjunction<T> {
602    /// Check whether the disjunction has no disjuncts, i.e., is `false`
603    pub fn is_empty(&self) -> bool {
604        self.0.is_empty()
605    }
606
607    /// Returns the number of elements in the disjunction
608    pub fn len(&self) -> usize {
609        self.0.len()
610    }
611
612    /// Create a new empty formula (which is equivalent to `false`)
613    pub fn new_bot() -> Self {
614        Self(Vec::new())
615    }
616
617    /// Check whether the formula is trivially equivalent to false
618    pub fn is_bot(&self) -> bool {
619        self.0.is_empty()
620    }
621
622    /// Get a reference to the disjunct at `index`
623    ///
624    /// Returns `None` if `index` is out of bounds.
625    pub fn get(&self, index: usize) -> Option<&T> {
626        self.0.get(index)
627    }
628
629    /// Get a mutable reference to the disjunct at `index`
630    ///
631    /// Returns `None` if `index` is out of bounds.
632    pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
633        self.0.get_mut(index)
634    }
635}
636
637impl<T: PartialEq> Index<usize> for Disjunction<T> {
638    type Output = T;
639
640    /// Access the disjunct at the given index
641    ///
642    /// # Panics
643    ///
644    /// Panics if `index` is out of bounds.
645    fn index(&self, index: usize) -> &Self::Output {
646        &self.0[index]
647    }
648}
649
650impl<T: PartialEq> IndexMut<usize> for Disjunction<T> {
651    /// Mutably access the disjunct at the given index
652    ///
653    /// # Panics
654    ///
655    /// Panics if `index` is out of bounds.
656    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
657        &mut self.0[index]
658    }
659}
660
661impl<T: PartialEq> AsRef<[T]> for Disjunction<T> {
662    fn as_ref(&self) -> &[T] {
663        &self.0
664    }
665}
666
667impl<T: PartialEq> AsMut<[T]> for Disjunction<T> {
668    fn as_mut(&mut self) -> &mut [T] {
669        &mut self.0
670    }
671}
672
673impl<T: PartialEq> Default for Disjunction<T> {
674    /// The empty disjunction, i.e., `false`
675    fn default() -> Self {
676        Self(Vec::new())
677    }
678}
679
680impl<T: PartialEq> PartialEq for Disjunction<T> {
681    /// Set equality: two disjunctions are equal if they contain the same
682    /// disjuncts, regardless of order or duplicates.
683    fn eq(&self, other: &Self) -> bool {
684        self.0.iter().all(|d| other.0.contains(d)) && other.0.iter().all(|d| self.0.contains(d))
685    }
686}
687
688impl<T: PartialEq> Or for Disjunction<T> {
689    fn or(mut self, other: Self) -> Self {
690        self.0.extend(other.0);
691        dedup(&mut self.0);
692        self
693    }
694}
695
696impl<T: fmt::Display + PartialEq> fmt::Display for Disjunction<T> {
697    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
698        write!(f, "{}", join_iterator(self.0.iter(), " || "))
699    }
700}
701
702impl<T: PartialEq, S: Into<Vec<T>>> From<S> for Disjunction<T> {
703    fn from(value: S) -> Self {
704        Self(value.into())
705    }
706}
707
708impl<T: PartialEq> Disjunction<T> {
709    /// Iterate over the disjuncts
710    pub fn iter(&self) -> std::slice::Iter<'_, T> {
711        self.0.iter()
712    }
713
714    /// Iterate over the disjuncts, allowing them to be modified
715    pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, T> {
716        self.0.iter_mut()
717    }
718}
719
720impl<T: PartialEq> IntoIterator for Disjunction<T> {
721    type Item = T;
722
723    type IntoIter = <Vec<T> as IntoIterator>::IntoIter;
724
725    fn into_iter(self) -> Self::IntoIter {
726        self.0.into_iter()
727    }
728}
729
730impl<'a, T: PartialEq> IntoIterator for &'a Disjunction<T> {
731    type Item = &'a T;
732
733    type IntoIter = std::slice::Iter<'a, T>;
734
735    fn into_iter(self) -> Self::IntoIter {
736        self.0.iter()
737    }
738}
739
740impl<'a, T: PartialEq> IntoIterator for &'a mut Disjunction<T> {
741    type Item = &'a mut T;
742
743    type IntoIter = std::slice::IterMut<'a, T>;
744
745    fn into_iter(self) -> Self::IntoIter {
746        self.0.iter_mut()
747    }
748}
749
750impl<T: PartialEq> FromIterator<T> for Disjunction<T> {
751    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
752        Self(iter.into_iter().collect())
753    }
754}
755
756impl<T: PartialEq> Conjunction<T> {
757    /// Check whether the conjunction has no conjuncts, i.e., is `true`
758    pub fn is_empty(&self) -> bool {
759        self.0.is_empty()
760    }
761
762    /// Returns the number of elements in the conjunction
763    pub fn len(&self) -> usize {
764        self.0.len()
765    }
766
767    /// Get a reference to the conjunct at `index`
768    ///
769    /// Returns `None` if `index` is out of bounds.
770    pub fn get(&self, index: usize) -> Option<&T> {
771        self.0.get(index)
772    }
773
774    /// Get a mutable reference to the conjunct at `index`
775    ///
776    /// Returns `None` if `index` is out of bounds.
777    pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
778        self.0.get_mut(index)
779    }
780}
781
782impl<T: PartialEq> Index<usize> for Conjunction<T> {
783    type Output = T;
784
785    /// Access the conjunct at the given index
786    ///
787    /// # Panics
788    ///
789    /// Panics if `index` is out of bounds.
790    fn index(&self, index: usize) -> &Self::Output {
791        &self.0[index]
792    }
793}
794
795impl<T: PartialEq> IndexMut<usize> for Conjunction<T> {
796    /// Mutably access the conjunct at the given index
797    ///
798    /// # Panics
799    ///
800    /// Panics if `index` is out of bounds.
801    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
802        &mut self.0[index]
803    }
804}
805
806impl<T: PartialEq> AsRef<[T]> for Conjunction<T> {
807    fn as_ref(&self) -> &[T] {
808        &self.0
809    }
810}
811
812impl<T: PartialEq> AsMut<[T]> for Conjunction<T> {
813    fn as_mut(&mut self) -> &mut [T] {
814        &mut self.0
815    }
816}
817
818impl<T: PartialEq> Default for Conjunction<T> {
819    /// The empty conjunction, i.e., `true`
820    fn default() -> Self {
821        Self(Vec::new())
822    }
823}
824
825impl<T: PartialEq> PartialEq for Conjunction<T> {
826    /// Set equality: two conjunctions are equal if they contain the same
827    /// conjuncts, regardless of order or duplicates.
828    fn eq(&self, other: &Self) -> bool {
829        self.0.iter().all(|d| other.0.contains(d)) && other.0.iter().all(|d| self.0.contains(d))
830    }
831}
832
833impl<T: PartialEq> And for Conjunction<T> {
834    fn and(mut self, other: Self) -> Self {
835        self.0.extend(other.0);
836        dedup(&mut self.0);
837        self
838    }
839}
840impl<T: fmt::Display + PartialEq> fmt::Display for Conjunction<T> {
841    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
842        write!(f, "{}", join_iterator(self.0.iter(), " && "))
843    }
844}
845
846impl<T: PartialEq, S: Into<Vec<T>>> From<S> for Conjunction<T> {
847    fn from(value: S) -> Self {
848        Self(value.into())
849    }
850}
851
852impl<T: PartialEq> Conjunction<T> {
853    /// Iterate over the conjuncts
854    pub fn iter(&self) -> std::slice::Iter<'_, T> {
855        self.0.iter()
856    }
857
858    /// Iterate over the conjuncts, allowing them to be modified
859    pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, T> {
860        self.0.iter_mut()
861    }
862}
863
864impl<T: PartialEq> IntoIterator for Conjunction<T> {
865    type Item = T;
866
867    type IntoIter = <Vec<T> as IntoIterator>::IntoIter;
868
869    fn into_iter(self) -> Self::IntoIter {
870        self.0.into_iter()
871    }
872}
873
874impl<'a, T: PartialEq> IntoIterator for &'a Conjunction<T> {
875    type Item = &'a T;
876
877    type IntoIter = std::slice::Iter<'a, T>;
878
879    fn into_iter(self) -> Self::IntoIter {
880        self.0.iter()
881    }
882}
883
884impl<'a, T: PartialEq> IntoIterator for &'a mut Conjunction<T> {
885    type Item = &'a mut T;
886
887    type IntoIter = std::slice::IterMut<'a, T>;
888
889    fn into_iter(self) -> Self::IntoIter {
890        self.0.iter_mut()
891    }
892}
893
894impl<T: PartialEq> FromIterator<T> for Conjunction<T> {
895    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
896        Self(iter.into_iter().collect())
897    }
898}
899
900#[cfg(test)]
901mod tests {
902    use taco_smt_encoder::SMTSolverBuilder;
903    use taco_threshold_automaton::{
904        ThresholdAutomaton,
905        expressions::{
906            And, BooleanConnective, BooleanExpression, ComparisonOp, IntegerExpression, Location,
907            Or, Parameter, Variable,
908        },
909        general_threshold_automaton::builder::GeneralThresholdAutomatonBuilder,
910        lia_threshold_automaton::{
911            LIAVariableConstraint, integer_thresholds::DeriveFromIntegerComp,
912        },
913    };
914
915    use crate::{
916        SpecificationTrait, TASpecification,
917        eltl::ELTLExpression,
918        internal_spec::{
919            Conjunction, Disjunction, ErrorAtom, ErrorFormula, ErrorSpec, ErrorTarget,
920            InitRestriction, upwards_closed_set::UpwardsClosedSet,
921        },
922    };
923
924    #[test]
925    fn test_try_from_eltl_untranslatable_atom() {
926        // l1 * l2 is not linear
927        let non_linear = |op| {
928            Box::new(ELTLExpression::LocationExpr(
929                Box::new(
930                    IntegerExpression::Atom(Location::new("l1"))
931                        * IntegerExpression::Atom(Location::new("l2")),
932                ),
933                op,
934                Box::new(IntegerExpression::Const(1)),
935            ))
936        };
937        let safety = ELTLExpression::Globally(non_linear(ComparisonOp::Lt));
938        let liveness = ELTLExpression::Eventually(non_linear(ComparisonOp::Gt));
939
940        // Liveness properties are reported as unknown
941        let (specs, unknown) = <ErrorSpec as SpecificationTrait<SMTSolverBuilder>>::try_from_eltl(
942            [("live".to_string(), liveness)].into_iter(),
943            &SMTSolverBuilder::default(),
944        )
945        .unwrap();
946        assert!(specs.is_empty());
947        assert_eq!(unknown, vec!["live".to_string()]);
948
949        // Errors in safety properties abort
950        let res = <ErrorSpec as SpecificationTrait<SMTSolverBuilder>>::try_from_eltl(
951            [("safe".to_string(), safety)].into_iter(),
952            &SMTSolverBuilder::default(),
953        );
954        assert!(res.is_err());
955    }
956
957    #[test]
958    fn test_error_spec_accessors_and_display() {
959        // [](l1 == 0)
960        let eltl = ELTLExpression::Globally(Box::new(ELTLExpression::LocationExpr(
961            Box::new(IntegerExpression::Atom(Location::new("l1"))),
962            ComparisonOp::Eq,
963            Box::new(IntegerExpression::Const(0)),
964        )));
965
966        // Violation: <>(l1 >= 1)
967        let ef = ErrorFormula::new(
968            InitRestriction::new_top(),
969            ErrorTarget::new_reach(UpwardsClosedSet::new_cover([Location::new("l1")])),
970        );
971
972        let spec = ErrorSpec {
973            name: "safety".to_string(),
974            source: Box::new(eltl.clone()),
975            ef: ef.clone(),
976        };
977
978        assert_eq!(spec.name(), "safety");
979        assert_eq!(spec.source(), &eltl);
980        assert_eq!(spec.error_formula(), &ef);
981        assert_eq!(spec.to_string(), "safety: (<>((l1 >= 1)))");
982    }
983
984    #[test]
985    fn test_create_tas_to_check() {
986        let l1 = Location::new("l1");
987        let l2 = Location::new("l2");
988        let x = Variable::new("x");
989        let n = Parameter::new("n");
990
991        let ta = GeneralThresholdAutomatonBuilder::new("ta".to_string())
992            .with_parameter(n.clone())
993            .unwrap()
994            .with_variable(x.clone())
995            .unwrap()
996            .with_locations([l1.clone(), l2.clone()])
997            .unwrap()
998            .initialize()
999            .build();
1000
1001        // n > 1
1002        let par_constr = BooleanExpression::ComparisonExpression(
1003            Box::new(IntegerExpression::Atom(n.clone())),
1004            ComparisonOp::Gt,
1005            Box::new(IntegerExpression::Const(1)),
1006        );
1007        // l1 == 0
1008        let loc_constr = BooleanExpression::ComparisonExpression(
1009            Box::new(IntegerExpression::Atom(l1.clone())),
1010            ComparisonOp::Eq,
1011            Box::new(IntegerExpression::Const(0)),
1012        );
1013        // x == 0
1014        let var_constr = BooleanExpression::ComparisonExpression(
1015            Box::new(IntegerExpression::Atom(x.clone())),
1016            ComparisonOp::Eq,
1017            Box::new(IntegerExpression::Const(0)),
1018        );
1019
1020        // (n > 1 && l1 == 0 && x == 0 && <>(l2 >= 1)) || [](l1 >= 1)
1021        let restricted = ErrorAtom::new(
1022            InitRestriction::new_parameter_constraint(par_constr.clone())
1023                & InitRestriction::new_location_constraint(loc_constr.clone())
1024                & InitRestriction::new_variable_constraint(var_constr.clone()),
1025            ErrorTarget::new_reach(UpwardsClosedSet::new_cover([l2.clone()])),
1026        );
1027        let unrestricted = ErrorAtom::new(
1028            InitRestriction::new_top(),
1029            ErrorTarget::new_invariant(UpwardsClosedSet::new_cover([l1.clone()])),
1030        );
1031
1032        let spec = ErrorSpec {
1033            name: "spec".to_string(),
1034            source: Box::new(ELTLExpression::True),
1035            ef: Disjunction::from([restricted.clone(), unrestricted.clone()]),
1036        };
1037
1038        let got: Vec<_> = spec.create_tas_to_check(&ta).collect();
1039        assert_eq!(got.len(), 2);
1040
1041        // The first atom adds its initial restriction to the automaton
1042        let (target, restricted_ta) = &got[0];
1043        assert_eq!(target, restricted.target());
1044        assert_eq!(restricted_ta.name(), "ta");
1045        assert_eq!(
1046            restricted_ta.resilience_conditions().collect::<Vec<_>>(),
1047            vec![&par_constr]
1048        );
1049        assert_eq!(
1050            restricted_ta
1051                .initial_location_constraints()
1052                .collect::<Vec<_>>(),
1053            vec![&loc_constr]
1054        );
1055        assert_eq!(
1056            restricted_ta
1057                .initial_variable_constraints()
1058                .collect::<Vec<_>>(),
1059            vec![&var_constr]
1060        );
1061
1062        // The second atom leaves the automaton unchanged
1063        let (target, unrestricted_ta) = &got[1];
1064        assert_eq!(target, unrestricted.target());
1065        assert_eq!(unrestricted_ta, &ta);
1066
1067        // `transform_threshold_automaton` pairs every atom of every
1068        // specification with a copy of the automaton
1069        let got =
1070            <ErrorSpec as SpecificationTrait<SMTSolverBuilder>>::transform_threshold_automaton(
1071                ta.clone(),
1072                vec![spec.clone(), spec],
1073                &SMTSolverBuilder::default(),
1074            );
1075        assert_eq!(got.len(), 4);
1076        assert_eq!(
1077            (got[0].0.as_str(), &got[0].1),
1078            ("spec", restricted.target())
1079        );
1080        assert_eq!(
1081            (got[1].0.as_str(), &got[1].1),
1082            ("spec", unrestricted.target())
1083        );
1084        assert_eq!(
1085            (got[2].0.as_str(), &got[2].1),
1086            ("spec", restricted.target())
1087        );
1088        assert_eq!(
1089            (got[3].0.as_str(), &got[3].1),
1090            ("spec", unrestricted.target())
1091        );
1092    }
1093
1094    #[test]
1095    fn test_error_atom_accessors_and_predicates() {
1096        // l1 == 0
1097        let loc_constr = BooleanExpression::ComparisonExpression(
1098            Box::new(IntegerExpression::Atom(Location::new("l1"))),
1099            ComparisonOp::Eq,
1100            Box::new(IntegerExpression::Const(0)),
1101        );
1102        let init = InitRestriction::new_location_constraint(loc_constr);
1103        let target = ErrorTarget::new_reach(UpwardsClosedSet::new_cover([Location::new("l2")]));
1104
1105        let atom = ErrorAtom::new(init.clone(), target.clone());
1106        assert_eq!(atom.init_restriction(), &init);
1107        assert_eq!(atom.target(), &target);
1108        assert!(!atom.is_top());
1109        assert!(!atom.is_bot());
1110
1111        // Only an atom without restriction and without temporal part is top
1112        assert!(ErrorAtom::new(InitRestriction::new_top(), ErrorTarget::Top).is_top());
1113        assert!(!ErrorAtom::new(init.clone(), ErrorTarget::Top).is_top());
1114        assert!(
1115            !ErrorAtom::new(
1116                InitRestriction::new_top(),
1117                ErrorTarget::new_reach(UpwardsClosedSet::new_top())
1118            )
1119            .is_top()
1120        );
1121
1122        // An atom is bot if the temporal part is unsatisfiable
1123        assert!(
1124            ErrorAtom::new(
1125                InitRestriction::new_top(),
1126                ErrorTarget::new_reach(UpwardsClosedSet::new_bot())
1127            )
1128            .is_bot()
1129        );
1130    }
1131
1132    #[test]
1133    fn test_error_atom_display() {
1134        let target = ErrorTarget::new_reach(UpwardsClosedSet::new_cover([Location::new("l2")]));
1135
1136        // Restriction is omitted if it is top
1137        let atom = ErrorAtom::new(InitRestriction::new_top(), target.clone());
1138        assert_eq!(atom.to_string(), "(<>((l2 >= 1)))");
1139
1140        // l1 == 0
1141        let loc_constr = BooleanExpression::ComparisonExpression(
1142            Box::new(IntegerExpression::Atom(Location::new("l1"))),
1143            ComparisonOp::Eq,
1144            Box::new(IntegerExpression::Const(0)),
1145        );
1146        let atom = ErrorAtom::new(InitRestriction::new_location_constraint(loc_constr), target);
1147        assert_eq!(atom.to_string(), "(l1 == 0) && (<>((l2 >= 1)))");
1148    }
1149
1150    #[test]
1151    fn test_ta_specification_for_atom_formula_and_target() {
1152        // x >= 1
1153        let x_geq_1 = LIAVariableConstraint::from_integer_expr(
1154            IntegerExpression::Atom(Variable::new("x")),
1155            ComparisonOp::Geq,
1156            IntegerExpression::Const(1),
1157        )
1158        .unwrap();
1159        // y >= 2
1160        let y_geq_2 = LIAVariableConstraint::from_integer_expr(
1161            IntegerExpression::Atom(Variable::new("y")),
1162            ComparisonOp::Geq,
1163            IntegerExpression::Const(2),
1164        )
1165        .unwrap();
1166
1167        let pre = UpwardsClosedSet::new_cover([Location::new("l1")])
1168            & UpwardsClosedSet::new_var_constraint(x_geq_1.clone());
1169        let inv = UpwardsClosedSet::new_cover([Location::new("l2")])
1170            & UpwardsClosedSet::new_var_constraint(y_geq_2.clone());
1171
1172        // Target with two sets: both are reported
1173        let target = ErrorTarget::new_ensure(pre.clone(), inv.clone());
1174        let mut locs: Vec<_> = target.locs_appearing().into_iter().collect();
1175        locs.sort();
1176        assert_eq!(locs, vec![&Location::new("l1"), &Location::new("l2")]);
1177        assert_eq!(
1178            target.var_constraint().into_iter().collect::<Vec<_>>(),
1179            vec![&x_geq_1, &y_geq_2]
1180        );
1181
1182        // Target with one set reports that set
1183        let reach = ErrorTarget::new_reach(pre.clone());
1184        assert_eq!(
1185            reach.locs_appearing().into_iter().collect::<Vec<_>>(),
1186            vec![&Location::new("l1")]
1187        );
1188        assert_eq!(
1189            reach.var_constraint().into_iter().collect::<Vec<_>>(),
1190            vec![&x_geq_1]
1191        );
1192
1193        // Target without temporal part reports nothing
1194        assert_eq!(ErrorTarget::Top.locs_appearing().into_iter().count(), 0);
1195        assert_eq!(ErrorTarget::Top.var_constraint().into_iter().count(), 0);
1196        let atom = ErrorAtom::new(InitRestriction::new_top(), ErrorTarget::Top);
1197        assert_eq!(atom.locs_appearing().into_iter().count(), 0);
1198        assert_eq!(atom.var_constraint().into_iter().count(), 0);
1199
1200        // The atom delegates to its target
1201        let atom = ErrorAtom::new(InitRestriction::new_top(), target);
1202        let mut locs: Vec<_> = atom.locs_appearing().into_iter().collect();
1203        locs.sort();
1204        assert_eq!(locs, vec![&Location::new("l1"), &Location::new("l2")]);
1205        assert_eq!(
1206            atom.var_constraint().into_iter().collect::<Vec<_>>(),
1207            vec![&x_geq_1, &y_geq_2]
1208        );
1209
1210        // The formula takes the union over all disjuncts
1211        let formula: ErrorFormula = Disjunction::from([
1212            ErrorAtom::new(InitRestriction::new_top(), ErrorTarget::new_reach(pre)),
1213            ErrorAtom::new(InitRestriction::new_top(), ErrorTarget::new_invariant(inv)),
1214        ]);
1215        let mut locs: Vec<_> = formula.locs_appearing().into_iter().collect();
1216        locs.sort();
1217        assert_eq!(locs, vec![&Location::new("l1"), &Location::new("l2")]);
1218        assert_eq!(
1219            formula.var_constraint().into_iter().collect::<Vec<_>>(),
1220            vec![&x_geq_1, &y_geq_2]
1221        );
1222    }
1223
1224    #[test]
1225    fn test_error_target_constructors() {
1226        let a = UpwardsClosedSet::new_cover([Location::new("l1")]);
1227        let b = UpwardsClosedSet::new_cover([Location::new("l2")]);
1228
1229        assert_eq!(
1230            ErrorTarget::new_reach(a.clone()),
1231            ErrorTarget::Reach(a.clone())
1232        );
1233        assert_eq!(
1234            ErrorTarget::new_invariant(a.clone()),
1235            ErrorTarget::Invariant(a.clone())
1236        );
1237        assert_eq!(
1238            ErrorTarget::new_ensure(a.clone(), b.clone()),
1239            ErrorTarget::Ensure {
1240                pre: a.clone(),
1241                inv: b.clone()
1242            }
1243        );
1244        assert_eq!(
1245            ErrorTarget::new_repeat(a.clone(), b.clone()),
1246            ErrorTarget::Repeat { pre: a, inv: b }
1247        );
1248    }
1249
1250    #[test]
1251    fn test_error_target_is_bot() {
1252        let set = UpwardsClosedSet::new_cover([Location::new("l1")]);
1253        let bot = UpwardsClosedSet::new_bot();
1254
1255        assert!(!ErrorTarget::new_reach(set.clone()).is_bot());
1256        assert!(ErrorTarget::new_reach(bot.clone()).is_bot());
1257
1258        assert!(!ErrorTarget::new_invariant(set.clone()).is_bot());
1259        assert!(ErrorTarget::new_invariant(bot.clone()).is_bot());
1260
1261        // A target with two sets is bot if one of them is bot
1262        assert!(!ErrorTarget::new_ensure(set.clone(), set.clone()).is_bot());
1263        assert!(ErrorTarget::new_ensure(bot.clone(), set.clone()).is_bot());
1264        assert!(ErrorTarget::new_ensure(set.clone(), bot.clone()).is_bot());
1265
1266        assert!(!ErrorTarget::new_repeat(set.clone(), set.clone()).is_bot());
1267        assert!(ErrorTarget::new_repeat(bot.clone(), set.clone()).is_bot());
1268        assert!(ErrorTarget::new_repeat(set, bot).is_bot());
1269
1270        assert!(!ErrorTarget::Top.is_bot());
1271    }
1272
1273    #[test]
1274    fn test_error_target_is_top() {
1275        let set = UpwardsClosedSet::new_cover([Location::new("l1")]);
1276        let top = UpwardsClosedSet::new_top();
1277
1278        assert!(!ErrorTarget::new_reach(set.clone()).is_top());
1279        assert!(ErrorTarget::new_reach(top.clone()).is_top());
1280
1281        assert!(!ErrorTarget::new_invariant(set.clone()).is_top());
1282        assert!(ErrorTarget::new_invariant(top.clone()).is_top());
1283
1284        // A target with two sets is top only if both of them are top
1285        assert!(ErrorTarget::new_ensure(top.clone(), top.clone()).is_top());
1286        assert!(!ErrorTarget::new_ensure(set.clone(), top.clone()).is_top());
1287        assert!(!ErrorTarget::new_ensure(top.clone(), set.clone()).is_top());
1288
1289        assert!(ErrorTarget::new_repeat(top.clone(), top.clone()).is_top());
1290        assert!(!ErrorTarget::new_repeat(set.clone(), top.clone()).is_top());
1291        assert!(!ErrorTarget::new_repeat(top, set).is_top());
1292
1293        assert!(ErrorTarget::Top.is_top());
1294    }
1295
1296    #[test]
1297    fn test_error_target_contains_reachability_constraint() {
1298        // l1 >= 1 && l2 == 0
1299        let reach = UpwardsClosedSet::new_reach([(Location::new("l1"), 1)], [Location::new("l2")]);
1300        // l1 >= 1
1301        let cover = UpwardsClosedSet::new_cover([Location::new("l1")]);
1302
1303        assert!(ErrorTarget::new_reach(reach.clone()).contains_reachability_constraint());
1304        assert!(!ErrorTarget::new_reach(cover.clone()).contains_reachability_constraint());
1305
1306        // Not implemented for the other temporal targets
1307        assert!(!ErrorTarget::new_invariant(reach.clone()).contains_reachability_constraint());
1308        assert!(
1309            !ErrorTarget::new_ensure(reach.clone(), reach.clone())
1310                .contains_reachability_constraint()
1311        );
1312        assert!(!ErrorTarget::new_repeat(reach.clone(), reach).contains_reachability_constraint());
1313
1314        assert!(!ErrorTarget::Top.contains_reachability_constraint());
1315    }
1316
1317    #[test]
1318    fn test_error_target_display() {
1319        let a = UpwardsClosedSet::new_cover([Location::new("l1")]);
1320        let b = UpwardsClosedSet::new_cover([Location::new("l2")]);
1321
1322        assert_eq!(
1323            ErrorTarget::new_reach(a.clone()).to_string(),
1324            "<>((l1 >= 1))"
1325        );
1326        assert_eq!(
1327            ErrorTarget::new_invariant(a.clone()).to_string(),
1328            "[]((l1 >= 1))"
1329        );
1330        assert_eq!(
1331            ErrorTarget::new_ensure(a.clone(), b.clone()).to_string(),
1332            "<>(((l1 >= 1)) && []((l2 >= 1)))"
1333        );
1334        assert_eq!(
1335            ErrorTarget::new_repeat(a, b).to_string(),
1336            "[](((l1 >= 1)) && <>((l2 >= 1)))"
1337        );
1338        assert_eq!(ErrorTarget::Top.to_string(), "true");
1339    }
1340
1341    #[test]
1342    fn test_init_restriction_constructors_and_accessors() {
1343        let top = InitRestriction::new_top();
1344        assert!(top.is_top());
1345        assert!(top.parameter_constraints().is_empty());
1346        assert!(top.location_constraints().is_empty());
1347        assert!(top.variable_constraints().is_empty());
1348
1349        // n > 1
1350        let par_constr = BooleanExpression::ComparisonExpression(
1351            Box::new(IntegerExpression::Atom(Parameter::new("n"))),
1352            ComparisonOp::Gt,
1353            Box::new(IntegerExpression::Const(1)),
1354        );
1355        let par = InitRestriction::new_parameter_constraint(par_constr.clone());
1356        assert!(!par.is_top());
1357        assert_eq!(
1358            par.parameter_constraints(),
1359            &Conjunction::from([par_constr])
1360        );
1361        assert!(par.location_constraints().is_empty());
1362        assert!(par.variable_constraints().is_empty());
1363
1364        // l1 == 0
1365        let loc_constr = BooleanExpression::ComparisonExpression(
1366            Box::new(IntegerExpression::Atom(Location::new("l1"))),
1367            ComparisonOp::Eq,
1368            Box::new(IntegerExpression::Const(0)),
1369        );
1370        let loc = InitRestriction::new_location_constraint(loc_constr.clone());
1371        assert!(!loc.is_top());
1372        assert!(loc.parameter_constraints().is_empty());
1373        assert_eq!(loc.location_constraints(), &Conjunction::from([loc_constr]));
1374        assert!(loc.variable_constraints().is_empty());
1375
1376        // x == 0
1377        let var_constr = BooleanExpression::ComparisonExpression(
1378            Box::new(IntegerExpression::Atom(Variable::new("x"))),
1379            ComparisonOp::Eq,
1380            Box::new(IntegerExpression::Const(0)),
1381        );
1382        let var = InitRestriction::new_variable_constraint(var_constr.clone());
1383        assert!(!var.is_top());
1384        assert!(var.parameter_constraints().is_empty());
1385        assert!(var.location_constraints().is_empty());
1386        assert_eq!(var.variable_constraints(), &Conjunction::from([var_constr]));
1387    }
1388
1389    #[test]
1390    fn test_init_restriction_and() {
1391        // n > 1
1392        let par_constr = BooleanExpression::ComparisonExpression(
1393            Box::new(IntegerExpression::Atom(Parameter::new("n"))),
1394            ComparisonOp::Gt,
1395            Box::new(IntegerExpression::Const(1)),
1396        );
1397        // l1 == 0
1398        let loc_constr = BooleanExpression::ComparisonExpression(
1399            Box::new(IntegerExpression::Atom(Location::new("l1"))),
1400            ComparisonOp::Eq,
1401            Box::new(IntegerExpression::Const(0)),
1402        );
1403        // l2 == 0
1404        let loc_constr_2 = BooleanExpression::ComparisonExpression(
1405            Box::new(IntegerExpression::Atom(Location::new("l2"))),
1406            ComparisonOp::Eq,
1407            Box::new(IntegerExpression::Const(0)),
1408        );
1409
1410        let got = InitRestriction::new_parameter_constraint(par_constr.clone())
1411            .and(InitRestriction::new_location_constraint(loc_constr.clone()))
1412            & InitRestriction::new_location_constraint(loc_constr_2.clone());
1413
1414        let expected = InitRestriction {
1415            precondition_par: [par_constr].into(),
1416            precondition_loc: [loc_constr.clone(), loc_constr_2].into(),
1417            precondition_var: Conjunction::default(),
1418        };
1419        assert_eq!(got, expected);
1420
1421        // Conjunction with top and with itself changes nothing
1422        let loc = InitRestriction::new_location_constraint(loc_constr);
1423        assert_eq!(loc.clone() & InitRestriction::new_top(), loc);
1424        assert_eq!(loc.clone() & loc.clone(), loc);
1425    }
1426
1427    #[test]
1428    fn test_init_restriction_try_or() {
1429        // n > 1
1430        let par_constr = BooleanExpression::ComparisonExpression(
1431            Box::new(IntegerExpression::Atom(Parameter::new("n"))),
1432            ComparisonOp::Gt,
1433            Box::new(IntegerExpression::Const(1)),
1434        );
1435        // l1 == 0
1436        let loc_constr_1 = BooleanExpression::ComparisonExpression(
1437            Box::new(IntegerExpression::Atom(Location::new("l1"))),
1438            ComparisonOp::Eq,
1439            Box::new(IntegerExpression::Const(0)),
1440        );
1441        // l2 == 0
1442        let loc_constr_2 = BooleanExpression::ComparisonExpression(
1443            Box::new(IntegerExpression::Atom(Location::new("l2"))),
1444            ComparisonOp::Eq,
1445            Box::new(IntegerExpression::Const(0)),
1446        );
1447        // x == 0
1448        let var_constr = BooleanExpression::ComparisonExpression(
1449            Box::new(IntegerExpression::Atom(Variable::new("x"))),
1450            ComparisonOp::Eq,
1451            Box::new(IntegerExpression::Const(0)),
1452        );
1453
1454        let par = InitRestriction::new_parameter_constraint(par_constr.clone());
1455        let loc_1 = InitRestriction::new_location_constraint(loc_constr_1.clone());
1456        let loc_2 = InitRestriction::new_location_constraint(loc_constr_2.clone());
1457        let var = InitRestriction::new_variable_constraint(var_constr.clone());
1458
1459        // `x || x` is `x`
1460        assert_eq!(loc_1.try_or(&loc_1), Some(loc_1.clone()));
1461
1462        // `(p && l1) || (p && l2)` is `p && (l1 || l2)`
1463        let got = (par.clone() & loc_1.clone()).try_or(&(par.clone() & loc_2.clone()));
1464        let expected = InitRestriction {
1465            precondition_par: [par_constr].into(),
1466            precondition_loc: [BooleanExpression::BinaryExpression(
1467                Box::new(loc_constr_1.clone()),
1468                BooleanConnective::Or,
1469                Box::new(loc_constr_2.clone()),
1470            )]
1471            .into(),
1472            precondition_var: Conjunction::default(),
1473        };
1474        assert_eq!(got, Some(expected));
1475
1476        // `(l1 && l2) || l1` is `(l1 && l2) || l1`, the conjunction is folded
1477        // into a single expression
1478        let got = (loc_1.clone() & loc_2.clone()).try_or(&loc_1);
1479        let expected = InitRestriction {
1480            precondition_par: Conjunction::default(),
1481            precondition_loc: [BooleanExpression::BinaryExpression(
1482                Box::new(BooleanExpression::BinaryExpression(
1483                    Box::new(loc_constr_1.clone()),
1484                    BooleanConnective::And,
1485                    Box::new(loc_constr_2.clone()),
1486                )),
1487                BooleanConnective::Or,
1488                Box::new(loc_constr_1.clone()),
1489            )]
1490            .into(),
1491            precondition_var: Conjunction::default(),
1492        };
1493        assert_eq!(got, Some(expected));
1494
1495        // `true || l1` is `true`
1496        assert_eq!(
1497            InitRestriction::new_top().try_or(&loc_1),
1498            Some(InitRestriction::new_top())
1499        );
1500        assert_eq!(
1501            loc_1.try_or(&InitRestriction::new_top()),
1502            Some(InitRestriction::new_top())
1503        );
1504
1505        // `(l1 && x) || (l2 && p)` differs in more than one kind
1506        assert_eq!((loc_1.clone() & var.clone()).try_or(&(loc_2 & par)), None);
1507
1508        // `l1 || x` differs in two kinds as well
1509        assert_eq!(loc_1.try_or(&var), None);
1510    }
1511
1512    #[test]
1513    fn test_init_restriction_display() {
1514        assert_eq!(InitRestriction::new_top().to_string(), "true");
1515
1516        // n > 1
1517        let par_constr = BooleanExpression::ComparisonExpression(
1518            Box::new(IntegerExpression::Atom(Parameter::new("n"))),
1519            ComparisonOp::Gt,
1520            Box::new(IntegerExpression::Const(1)),
1521        );
1522        // l1 == 0
1523        let loc_constr = BooleanExpression::ComparisonExpression(
1524            Box::new(IntegerExpression::Atom(Location::new("l1"))),
1525            ComparisonOp::Eq,
1526            Box::new(IntegerExpression::Const(0)),
1527        );
1528        // x == 0
1529        let var_constr = BooleanExpression::ComparisonExpression(
1530            Box::new(IntegerExpression::Atom(Variable::new("x"))),
1531            ComparisonOp::Eq,
1532            Box::new(IntegerExpression::Const(0)),
1533        );
1534
1535        assert_eq!(
1536            InitRestriction::new_location_constraint(loc_constr.clone()).to_string(),
1537            "l1 == 0"
1538        );
1539
1540        // Empty kinds are skipped
1541        let all = InitRestriction::new_parameter_constraint(par_constr)
1542            & InitRestriction::new_location_constraint(loc_constr)
1543            & InitRestriction::new_variable_constraint(var_constr);
1544        assert_eq!(all.to_string(), "n > 1 && l1 == 0 && x == 0");
1545    }
1546
1547    #[test]
1548    fn test_disjunction_construction_and_predicates() {
1549        let bot: Disjunction<i32> = Disjunction::new_bot();
1550        assert!(bot.is_bot());
1551        assert!(bot.is_empty());
1552        assert_eq!(bot.len(), 0);
1553        assert_eq!(bot, Disjunction::default());
1554
1555        let disj = Disjunction::from([1, 2, 3]);
1556        assert!(!disj.is_bot());
1557        assert!(!disj.is_empty());
1558        assert_eq!(disj.len(), 3);
1559
1560        // `From` and `FromIterator` give the same result
1561        assert_eq!(disj, [1, 2, 3].into_iter().collect::<Disjunction<_>>());
1562    }
1563
1564    #[test]
1565    fn test_disjunction_element_access() {
1566        let mut disj = Disjunction::from([1, 2, 3]);
1567
1568        assert_eq!(disj.get(1), Some(&2));
1569        assert_eq!(disj.get(3), None);
1570        assert_eq!(disj[0], 1);
1571        assert_eq!(disj.as_ref(), &[1, 2, 3]);
1572
1573        *disj.get_mut(1).unwrap() = 20;
1574        assert_eq!(disj.get_mut(3), None);
1575        disj[0] = 10;
1576        disj.as_mut()[2] = 30;
1577        assert_eq!(disj.as_ref(), &[10, 20, 30]);
1578    }
1579
1580    #[test]
1581    fn test_disjunction_iteration() {
1582        let mut disj = Disjunction::from([1, 2, 3]);
1583
1584        assert_eq!(disj.iter().copied().collect::<Vec<_>>(), vec![1, 2, 3]);
1585        assert_eq!(
1586            (&disj).into_iter().copied().collect::<Vec<_>>(),
1587            vec![1, 2, 3]
1588        );
1589
1590        disj.iter_mut().for_each(|d| *d += 1);
1591        assert_eq!(disj.as_ref(), &[2, 3, 4]);
1592
1593        (&mut disj).into_iter().for_each(|d| *d *= 10);
1594        assert_eq!(disj.as_ref(), &[20, 30, 40]);
1595
1596        assert_eq!(disj.into_iter().collect::<Vec<_>>(), vec![20, 30, 40]);
1597    }
1598
1599    #[test]
1600    fn test_disjunction_eq_or_and_display() {
1601        // Equality ignores order and duplicates
1602        assert_eq!(Disjunction::from([1, 2]), Disjunction::from([2, 1, 1]));
1603        assert_ne!(Disjunction::from([1, 2]), Disjunction::from([1]));
1604
1605        // `Or` concatenates and removes duplicates, keeping the first occurrence
1606        let got = Disjunction::from([1, 2]).or(Disjunction::from([2, 3]));
1607        assert_eq!(got.as_ref(), &[1, 2, 3]);
1608
1609        assert_eq!(Disjunction::from([1, 2, 3]).to_string(), "1 || 2 || 3");
1610        assert_eq!(Disjunction::<i32>::new_bot().to_string(), "");
1611    }
1612
1613    #[test]
1614    fn test_conjunction_construction_and_predicates() {
1615        let top: Conjunction<i32> = Conjunction::default();
1616        assert!(top.is_empty());
1617        assert_eq!(top.len(), 0);
1618
1619        let conj = Conjunction::from([1, 2, 3]);
1620        assert!(!conj.is_empty());
1621        assert_eq!(conj.len(), 3);
1622
1623        // `From` and `FromIterator` give the same result
1624        assert_eq!(conj, [1, 2, 3].into_iter().collect::<Conjunction<_>>());
1625    }
1626
1627    #[test]
1628    fn test_conjunction_element_access() {
1629        let mut conj = Conjunction::from([1, 2, 3]);
1630
1631        assert_eq!(conj.get(1), Some(&2));
1632        assert_eq!(conj.get(3), None);
1633        assert_eq!(conj[0], 1);
1634        assert_eq!(conj.as_ref(), &[1, 2, 3]);
1635
1636        *conj.get_mut(1).unwrap() = 20;
1637        assert_eq!(conj.get_mut(3), None);
1638        conj[0] = 10;
1639        conj.as_mut()[2] = 30;
1640        assert_eq!(conj.as_ref(), &[10, 20, 30]);
1641    }
1642
1643    #[test]
1644    fn test_conjunction_iteration() {
1645        let mut conj = Conjunction::from([1, 2, 3]);
1646
1647        assert_eq!(conj.iter().copied().collect::<Vec<_>>(), vec![1, 2, 3]);
1648        assert_eq!(
1649            (&conj).into_iter().copied().collect::<Vec<_>>(),
1650            vec![1, 2, 3]
1651        );
1652
1653        conj.iter_mut().for_each(|c| *c += 1);
1654        assert_eq!(conj.as_ref(), &[2, 3, 4]);
1655
1656        (&mut conj).into_iter().for_each(|c| *c *= 10);
1657        assert_eq!(conj.as_ref(), &[20, 30, 40]);
1658
1659        assert_eq!(conj.into_iter().collect::<Vec<_>>(), vec![20, 30, 40]);
1660    }
1661
1662    #[test]
1663    fn test_conjunction_eq_and_and_display() {
1664        // Equality ignores order and duplicates
1665        assert_eq!(Conjunction::from([1, 2]), Conjunction::from([2, 1, 1]));
1666        assert_ne!(Conjunction::from([1, 2]), Conjunction::from([1]));
1667
1668        // `And` concatenates and removes duplicates, keeping the first occurrence
1669        let got = Conjunction::from([1, 2]).and(Conjunction::from([2, 3]));
1670        assert_eq!(got.as_ref(), &[1, 2, 3]);
1671
1672        assert_eq!(Conjunction::from([1, 2, 3]).to_string(), "1 && 2 && 3");
1673        assert_eq!(Conjunction::<i32>::default().to_string(), "");
1674    }
1675}