1
//! Authors: Jan Friso Groote, Maurice Laveaux, Wieger Wesselink and Tim A.C.
2
//! Willemse
3
//!
4
//! > M. Laveaux, J.F. Groote and T.A.C. Willemse. Correct and Efficient
5
//! > Antichain Algorithms for Refinement Checking. Logical Methods in Computer
6
//! > Science 17(1) 2021
7
//!
8
//! There are six algorithms. One for trace inclusion, one for failures
9
//! inclusion and one for failures-divergence inclusion. All algorithms come in
10
//! a variant with and without internal steps. It is possible to generate a
11
//! counter transition system in case the inclusion is answered by no.
12

            
13
use std::collections::HashSet;
14
use std::collections::VecDeque;
15

            
16
use log::trace;
17
use merc_collections::VecSet;
18
use merc_lts::LTS;
19
use merc_lts::StateIndex;
20
use merc_reduction::Equivalence;
21
use merc_reduction::Partition;
22
use merc_reduction::quotient_lts_block;
23
use merc_reduction::reduce_lts;
24
use merc_reduction::strong_bisim_sigref;
25
use merc_utilities::Timing;
26

            
27
use crate::Antichain;
28
use crate::CounterExample;
29
use crate::CounterExampleConstructor;
30
use crate::CounterExampleTree;
31
use crate::RefinementType;
32

            
33
/// Sets the exploration strategy for the failures refinement algorithm.
34
pub enum ExplorationStrategy {
35
    BFS,
36
    DFS,
37
}
38

            
39
/// This function checks using algorithms in the paper mentioned above whether
40
/// transition system l1 is included in transition system l2.
41
///
42
/// # Details
43
///
44
/// The `refinement_type` determines (weak) trace inclusions, failures inclusion
45
/// and divergence failures inclusion etc.
46
///
47
/// The `strategy` parameter determines whether a breadth-first search
48
/// or depth-first search is used to explore the state space. Breadth-first search
49
/// is often better suited for finding short counter examples, while depth-first
50
/// search often uses less memory.
51
///
52
/// The `preprocess` flag indicates whether preprocessing should be applied to
53
/// the LTSs. The refinement checks often involve product constructions, and
54
/// reducing the state space beforehand can lead to significant performance
55
/// improvements. However, for quick failing checks the preprocessing could cause
56
/// unnecessary overhead.
57
200
pub fn is_failures_refinement<L: LTS>(
58
200
    impl_lts: L,
59
200
    spec_lts: L,
60
200
    refinement: RefinementType,
61
200
    strategy: ExplorationStrategy,
62
200
    preprocess: bool,
63
200
    counter_example: bool,
64
200
    timing: &mut Timing,
65
200
) -> (bool, Option<CounterExample<L::Label>>) {
66
200
    let reduction = match refinement {
67
200
        RefinementType::Trace => Equivalence::StrongBisim,
68
        RefinementType::Weaktrace => Equivalence::BranchingBisim,
69
    };
70

            
71
    // For the preprocessing/quotienting step it makes sense to merge both LTSs
72
    // together in case that some states are equivalent. So we do this in all branches.
73
200
    let (merged_lts, initial_spec) = if preprocess {
74
100
        if counter_example {
75
            // If a counter example is to be generated, we only reduce the
76
            // specification LTS such that the resulting counter example remains valid.
77
100
            let reduced_spec = reduce_lts(spec_lts, reduction, true, timing);
78
100
            impl_lts.merge_disjoint(&reduced_spec)
79
        } else {
80
            let (merged_lts, initial_spec) = impl_lts.merge_disjoint(&spec_lts);
81

            
82
            // Reduce all states in the merged LTS.
83
            match reduction {
84
                Equivalence::StrongBisim => {
85
                    let (preprocess_lts, partition) = strong_bisim_sigref(merged_lts, timing);
86

            
87
                    let initial_spec = partition.block_number(initial_spec);
88
                    let reduced_lts = quotient_lts_block::<_, false>(&preprocess_lts, &partition);
89

            
90
                    // After partitioning the block becomes the state in the reduced_lts.
91
                    (reduced_lts, StateIndex::new(*initial_spec))
92
                }
93
                _ => unimplemented!(),
94
            }
95
        }
96
    } else {
97
100
        impl_lts.merge_disjoint(&spec_lts)
98
    };
99

            
100
200
    timing.measure("refinement", || {
101
200
        if counter_example {
102
            // Construct a counter example tree, and return a trace.
103
200
            let mut ce_constructor = CounterExampleConstructor::new();
104
200
            let (result, state) =
105
200
                is_refinement_internal(strategy, refinement, &merged_lts, initial_spec, &mut ce_constructor);
106
200
            trace!("Counter example tree: {:?}", ce_constructor);
107

            
108
200
            if let Some(state) = state {
109
                // Reconstruct a trace from the counter example tree, relabelling the indices to their actual labels.
110
                (
111
178
                    result,
112
178
                    Some(if refinement == RefinementType::Trace {
113
                        CounterExample::Trace(
114
178
                            ce_constructor
115
178
                                .reconstruct_trace(state)
116
178
                                .iter()
117
194
                                .map(|l| merged_lts.labels()[*l].clone())
118
178
                                .collect(),
119
                        )
120
                    } else {
121
                        // The resulting trace is a weak trace.
122
                        CounterExample::WeakTrace(
123
                            ce_constructor
124
                                .reconstruct_trace(state)
125
                                .iter()
126
                                .map(|l| merged_lts.labels()[*l].clone())
127
                                .collect(),
128
                        )
129
                    }),
130
                )
131
            } else {
132
22
                (result, None)
133
            }
134
        } else {
135
            // Run without constructing a counter example.
136
            let (result, _) = is_refinement_internal::<_, ()>(strategy, refinement, &merged_lts, initial_spec, &mut ());
137
            (result, None)
138
        }
139
200
    })
140
200
}
141

            
142
/// The inner loop for checking refinement.
143
///
144
/// # Details
145
///
146
/// This is mostly used internally. The `CE` type parameter indicates the type
147
/// of counter example tree that is used to construct counter examples. If no
148
/// counter examples are required, this can be set to `()`. Avoiding the cost
149
/// for keeping track of counter example information.
150
200
fn is_refinement_internal<L: LTS, CE: CounterExampleTree>(
151
200
    strategy: ExplorationStrategy,
152
200
    refinement: RefinementType,
153
200
    merged_lts: &L,
154
200
    initial_spec: StateIndex,
155
200
    counter_example: &mut CE,
156
200
) -> (bool, Option<CE::Index>) {
157
200
    let mut working = VecDeque::from([(
158
200
        merged_lts.initial_state_index(),
159
200
        VecSet::singleton(initial_spec),
160
200
        counter_example.root_index(),
161
200
    )]);
162

            
163
    // The antichain data structure is used for storing explored states. However, as opposed to a discovered set it
164
    // allows for pruning additional pairs based on the `antichain` property.
165
200
    let mut antichain = Antichain::new();
166

            
167
    // Cache used for the tau-closure computations.
168
200
    let mut closure_cache = ClosureCache::new();
169

            
170
250
    while let Some((impl_state, spec, ce)) = working.pop_front() {
171
228
        trace!("Checking ({:?}, {:?})", impl_state, spec);
172
        // pop (impl,spec) from working;
173

            
174
        // for all impl-e->impl' do
175
294
        for impl_transition in merged_lts.outgoing_transitions(impl_state) {
176
294
            let new_edge = counter_example.add_edge(impl_transition.label, ce);
177

            
178
294
            let spec_prime =
179
294
                if refinement == RefinementType::Weaktrace && merged_lts.is_hidden_label(impl_transition.label) {
180
                    // spec' := spec if e == tau
181
                    spec.clone()
182
                } else {
183
                    // spec' := {s' | exists s in spec. s-e->s'};
184
294
                    let mut spec_prime = VecSet::new();
185

            
186
294
                    if refinement == RefinementType::Weaktrace {
187
                        // For weak trace refinement we need to consider tau-closures s -->* s1 -e-> s2 -*-> s'
188
                        let closure = tau_closure(merged_lts, spec.clone().to_vec(), true, &mut closure_cache);
189

            
190
                        for s in &closure {
191
                            for spec_transition in merged_lts.outgoing_transitions(*s) {
192
                                if impl_transition.label == spec_transition.label {
193
                                    spec_prime.insert(spec_transition.to);
194
                                }
195
                            }
196
                        }
197

            
198
                        spec_prime =
199
                            VecSet::from_vec(tau_closure(merged_lts, spec_prime.to_vec(), false, &mut closure_cache));
200
                    } else {
201
                        // Otherwise, simply consider direct transitions.
202
299
                        for s in &spec {
203
791
                            for spec_transition in merged_lts.outgoing_transitions(*s) {
204
791
                                if impl_transition.label == spec_transition.label {
205
146
                                    spec_prime.insert(spec_transition.to);
206
645
                                }
207
                            }
208
                        }
209
                    }
210

            
211
294
                    spec_prime
212
                };
213

            
214
294
            trace!("spec' = {:?}", spec_prime);
215
294
            if spec_prime.is_empty() {
216
                // if spec' = {} then
217
178
                return (false, Some(new_edge));
218
116
            }
219

            
220
116
            if antichain.insert(impl_transition.to, spec_prime.clone()) {
221
                // if antichain_insert(impl,spec') then
222
112
                match strategy {
223
112
                    ExplorationStrategy::BFS => working.push_back((impl_transition.to, spec_prime, new_edge)),
224
                    ExplorationStrategy::DFS => working.push_front((impl_transition.to, spec_prime, new_edge)),
225
                }
226
4
            }
227
        }
228
    }
229

            
230
22
    (true, None)
231
200
}
232

            
233
/// A cache that is used to reuse allocations during tau-closure computations.
234
pub struct ClosureCache {
235
    /// States that are still to be explored.
236
    working: Vec<StateIndex>,
237

            
238
    /// Set of already visited states.
239
    visited: HashSet<StateIndex>,
240
}
241

            
242
impl ClosureCache {
243
    /// Creates a new closure cache.
244
200
    pub fn new() -> Self {
245
200
        ClosureCache {
246
200
            working: Vec::new(),
247
200
            visited: HashSet::new(),
248
200
        }
249
200
    }
250
}
251

            
252
impl Default for ClosureCache {
253
    fn default() -> Self {
254
        Self::new()
255
    }
256
}
257

            
258
/// Returns the tau closure for a set of states in the given LTS.
259
///
260
/// # Details
261
///
262
/// The `states` parameter indicates the initial set of states for which the
263
/// tau-closure is to be computed. The `extend` parameter indicates whether the
264
/// closure should include the original states as well. The `cache` parameter is
265
/// used to avoid repeated allocations.
266
fn tau_closure<L: LTS>(
267
    lts: &L,
268
    mut states: Vec<StateIndex>,
269
    extend: bool,
270
    cache: &mut ClosureCache,
271
) -> Vec<StateIndex> {
272
    debug_assert_eq!(cache.working.len(), 0, "Closure cache not cleared before use.");
273
    debug_assert_eq!(cache.visited.len(), 0, "Closure cache not cleared before use.");
274

            
275
    if extend {
276
        cache.working.extend(states.iter().cloned());
277
    } else {
278
        // Leaves the states empty.
279
        cache.working.append(&mut states);
280
    }
281

            
282
    // Keep track of states that are already in the closure.
283
    for s in &states {
284
        cache.visited.insert(*s);
285
    }
286

            
287
    while let Some(s) = cache.working.pop() {
288
        for t in lts.outgoing_transitions(s) {
289
            if lts.is_hidden_label(t.label) && !cache.visited.contains(&t.to) {
290
                cache.working.push(t.to);
291
                cache.visited.insert(t.to);
292
                states.push(t.to);
293
            }
294
        }
295
    }
296

            
297
    // Clear the cache for the next use, the working set is empty by now.
298
    cache.visited.clear();
299

            
300
    states
301
}
302

            
303
#[cfg(test)]
304
mod tests {
305
    use merc_io::DumpFiles;
306
    use merc_lts::random_lts;
307
    use merc_lts::write_aut;
308
    use merc_utilities::Timing;
309
    use merc_utilities::random_test;
310
    use merc_vpg::solve_zielonka;
311
    use merc_vpg::translate;
312

            
313
    use crate::ExplorationStrategy;
314
    use crate::RefinementType;
315
    use crate::generate_formula;
316
    use crate::is_failures_refinement;
317

            
318
    #[test]
319
    #[cfg_attr(miri, ignore)] // Tests are too slow under miri.
320
1
    fn test_random_trace_refinement() {
321
100
        random_test(100, |rng| {
322
100
            let mut files = DumpFiles::new("test_random_trace_refinement");
323

            
324
100
            let spec_lts = random_lts(rng, 10, 10, 3);
325
100
            let impl_lts = random_lts(rng, 10, 10, 3);
326

            
327
100
            files.dump("spec.aut", |w| write_aut(w, &spec_lts)).unwrap();
328
100
            files.dump("impl.aut", |w| write_aut(w, &impl_lts)).unwrap();
329

            
330
200
            for preprocess in [false, true] {
331
200
                let mut timing = Timing::default();
332
200
                let (result, counter_example) = is_failures_refinement(
333
200
                    impl_lts.clone(),
334
200
                    spec_lts.clone(),
335
200
                    RefinementType::Trace,
336
200
                    ExplorationStrategy::BFS,
337
200
                    preprocess,
338
200
                    true,
339
200
                    &mut timing,
340
200
                );
341

            
342
200
                if !result {
343
178
                    if let Some(ce) = counter_example {
344
178
                        let formula = generate_formula(&ce);
345
178
                        println!("Counter example formula: {}", formula);
346

            
347
178
                        let impl_pg = translate(&impl_lts, &formula).unwrap();
348
178
                        let spec_pg = translate(&spec_lts, &formula).unwrap();
349

            
350
178
                        let (impl_solution, _) = solve_zielonka(&impl_pg);
351
178
                        let (spec_solution, _) = solve_zielonka(&spec_pg);
352

            
353
178
                        assert!(
354
178
                            impl_solution[0] != spec_solution[0],
355
                            "Refinement returned false, but the counter example is not distinguishing."
356
                        );
357
                    } else {
358
                        panic!("Expected a counter example.");
359
                    }
360
22
                }
361
            }
362
100
        });
363
1
    }
364
}