1
use itertools::Itertools;
2
use log::debug;
3
use log::trace;
4
use merc_lts::LTS;
5
use merc_lts::StateIndex;
6
use merc_reduction::Equivalence;
7
use merc_reduction::Partition;
8
use merc_reduction::branching_bisim_sigref;
9
use merc_reduction::quotient_lts_block;
10
use merc_reduction::quotient_lts_naive;
11
use merc_reduction::strong_bisim_sigref;
12
use merc_reduction::tau_scc_decomposition;
13
use merc_utilities::Timing;
14

            
15
use crate::CounterExample;
16
use crate::CounterExampleConstructor;
17
use crate::ImpossibleFuturesResult;
18
use crate::InnerCe;
19
use crate::is_failures_refinement;
20
use crate::is_impossible_futures_refinement;
21

            
22
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
24
pub enum RefinementType {
25
    /// Checks for (strong) trace inclusion, i.e., whether all traces of the implementation are also traces of the specification.
26
    Trace,
27
    /// Checks for weak trace inclusion, i.e., whether all weak traces of the implementation are also weak traces of the specification.
28
    Weaktrace,
29
    /// Checks for stable failures inclusion, i.e., whether all stable failures of the implementation are also stable failures of the specification.
30
    StableFailures,
31
    /// Check for failures-divergences inclusion, i.e., whether all failures and divergences of the implementation are also failures and divergences of the specification.
32
    FailuresDivergences,
33
    /// Checks for impossible futures inclusion, i.e., whether all impossible futures of the implementation are also impossible futures of the specification.
34
    ImpossibleFutures,
35
}
36

            
37
/// Determines the exploration strategy for the failures refinement algorithm.
38
///
39
/// Typically `BFS` is more suited for counter examples, but `DFS` can be more efficient
40
/// in practice when a counter example is not required.
41
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42
pub enum ExplorationStrategy {
43
    /// Breadth-first search; better suited for finding short counter examples.
44
    BFS,
45
    /// Depth-first search; often uses less memory when no counter example is required.
46
    DFS,
47
}
48

            
49
/// Checks whether `impl_lts` refines `spec_lts` according to the given
50
/// `refinement` relation.
51
///
52
/// # Details
53
///
54
/// The `refinement_type` determines (weak) trace inclusions, failures inclusion
55
/// and divergence failures inclusion etc.
56
///
57
/// The `strategy` parameter determines whether a breadth-first search
58
/// or depth-first search is used to explore the state space. Breadth-first search
59
/// is often better suited for finding short counter examples, while depth-first
60
/// search often uses less memory.
61
///
62
/// The `preprocess` flag indicates whether preprocessing should be applied to
63
/// the LTSs. The refinement checks often involve product constructions, and
64
/// reducing the state space beforehand can lead to significant performance
65
/// improvements. However, for quick failing checks the preprocessing could cause
66
/// unnecessary overhead.
67
1009
pub fn refines<L: LTS>(
68
1009
    impl_lts: L,
69
1009
    spec_lts: L,
70
1009
    refinement: RefinementType,
71
1009
    strategy: ExplorationStrategy,
72
1009
    preprocess: bool,
73
1009
    counter_example: bool,
74
1009
    timing: &mut Timing,
75
1009
) -> (bool, Option<CounterExample<L::Label>>) {
76
1009
    let reduction = match refinement {
77
200
        RefinementType::Trace => Equivalence::StrongBisim,
78
        // Note that for impossible futures we use branching bisimulation, which also removes tau loops.
79
407
        RefinementType::Weaktrace | RefinementType::ImpossibleFutures => Equivalence::BranchingBisim,
80
        RefinementType::StableFailures | RefinementType::FailuresDivergences => {
81
402
            Equivalence::BranchingBisimDivergencePreserving
82
        }
83
    };
84

            
85
    // For the preprocessing/quotienting step it makes sense to merge both LTSs
86
    // together in case that some states are equivalent. So we do this in all branches.
87
1009
    let (merged_lts, initial_spec) = if preprocess {
88
        // Reduce all states in the merged LTS.
89
503
        match reduction {
90
            Equivalence::StrongBisim => {
91
100
                let (merged_lts, initial_spec) = impl_lts.merge_disjoint(&spec_lts);
92
100
                let (preprocess_lts, partition) = strong_bisim_sigref(merged_lts, timing);
93

            
94
100
                let impl_block = partition.block_number(preprocess_lts.initial_state_index());
95
100
                let spec_block = partition.block_number(initial_spec);
96

            
97
100
                if impl_block == spec_block {
98
                    // The initial states are already in the same block, so we can skip the refinement check.
99
25
                    debug!(
100
                        "Initial states are in the same block after strong bisimulation reduction, skipping refinement check."
101
                    );
102
25
                    return (true, None);
103
75
                }
104

            
105
                // After partitioning the block becomes the state in the reduced_lts.
106
75
                let reduced_lts = quotient_lts_block::<_, false>(&preprocess_lts, &partition, false);
107
75
                (reduced_lts, StateIndex::new(*spec_block))
108
            }
109
            Equivalence::BranchingBisim | Equivalence::BranchingBisimDivergencePreserving => {
110
403
                let (merged_lts, initial_spec) = impl_lts.merge_disjoint(&spec_lts);
111
403
                let (preprocess_lts, initial_spec, partition) = branching_bisim_sigref(
112
403
                    merged_lts,
113
403
                    initial_spec,
114
403
                    reduction == Equivalence::BranchingBisimDivergencePreserving,
115
403
                    timing,
116
403
                );
117

            
118
403
                let impl_block = partition.block_number(preprocess_lts.initial_state_index());
119
403
                let spec_block = partition.block_number(initial_spec);
120

            
121
403
                if impl_block == spec_block {
122
                    // The initial states are already in the same block, so we can skip the refinement check.
123
119
                    debug!(
124
                        "Initial states are in the same block after branching bisimulation reduction, skipping refinement check."
125
                    );
126
119
                    return (true, None);
127
284
                }
128

            
129
284
                let reduced_lts = quotient_lts_block::<_, true>(
130
284
                    &preprocess_lts,
131
284
                    &partition,
132
284
                    reduction != Equivalence::BranchingBisimDivergencePreserving,
133
                );
134
284
                (reduced_lts, StateIndex::new(*spec_block))
135
            }
136
            _ => unimplemented!("Preprocessing for refinement type {refinement:?} has not been implemented yet."),
137
        }
138
506
    } else if refinement == RefinementType::ImpossibleFutures {
139
        // For impossible futures we need to remove tau loops from the implementation.
140
101
        let scc_partition = tau_scc_decomposition(&impl_lts);
141
101
        let tau_loop_free_lts = quotient_lts_naive(&impl_lts, &scc_partition, true, true);
142

            
143
101
        tau_loop_free_lts.merge_disjoint(&spec_lts)
144
    } else {
145
405
        impl_lts.merge_disjoint(&spec_lts)
146
    };
147

            
148
    // Print the labels of the merged LTS for debugging purposes.
149
865
    trace!(
150
        "Merged LTS labels: {:?}",
151
        merged_lts.labels().iter().enumerate().format("\n")
152
    );
153

            
154
865
    timing.measure("refinement", || {
155
865
        if counter_example {
156
            // Construct a counter example tree, and return a trace.
157
856
            let mut ce_constructor = CounterExampleConstructor::new();
158
856
            let result = match refinement {
159
                RefinementType::Trace
160
                | RefinementType::Weaktrace
161
                | RefinementType::StableFailures
162
                | RefinementType::FailuresDivergences => {
163
691
                    let (result, ce_state, ce_inner) =
164
691
                        is_failures_refinement(&merged_lts, initial_spec, refinement, strategy, &mut ce_constructor);
165

            
166
691
                    if let Some(state) = ce_state {
167
                        // Reconstruct a trace from the counter example tree, relabelling the indices to their actual labels.
168
464
                        let trace = ce_constructor
169
464
                            .reconstruct_trace(state)
170
464
                            .iter()
171
689
                            .map(|l| merged_lts.labels()[*l].clone())
172
464
                            .collect();
173
                        (
174
464
                            result,
175
464
                            Some(match refinement {
176
107
                                RefinementType::Trace => CounterExample::Trace(trace),
177
95
                                RefinementType::Weaktrace => CounterExample::WeakTrace(trace),
178
                                RefinementType::StableFailures | RefinementType::FailuresDivergences => {
179
262
                                    if let Some(inner) = ce_inner {
180
120
                                        match inner {
181
1
                                            InnerCe::Diverges => CounterExample::Divergence(trace),
182
119
                                            InnerCe::Refusal(refusal) => CounterExample::StableFailures(
183
119
                                                trace,
184
207
                                                refusal.iter().map(|l| merged_lts.labels()[*l].clone()).collect(),
185
                                            ),
186
                                        }
187
                                    } else {
188
                                        // The stable failures failed because of a weak trace difference.
189
142
                                        CounterExample::WeakTrace(trace)
190
                                    }
191
                                }
192
                                _ => unreachable!("Refinement {refinement:?} is not valid in this path"),
193
                            }),
194
                        )
195
                    } else {
196
227
                        (result, None)
197
                    }
198
                }
199
                RefinementType::ImpossibleFutures => {
200
                    let ImpossibleFuturesResult {
201
165
                        result,
202
165
                        counter_example: ce_state,
203
165
                        impossible_futures: ce_inner,
204
165
                    } = is_impossible_futures_refinement(&merged_lts, initial_spec, strategy, &mut ce_constructor);
205

            
206
165
                    if let Some(state) = ce_state {
207
                        // Reconstruct a trace from the counter example tree, relabelling the indices to their actual labels.
208
126
                        let trace = ce_constructor
209
126
                            .reconstruct_trace(state)
210
126
                            .iter()
211
134
                            .map(|l| merged_lts.labels()[*l].clone())
212
126
                            .collect();
213
                        (
214
126
                            result,
215
126
                            Some(if let Some(inner) = ce_inner {
216
64
                                CounterExample::ImpossibleFutures(trace, inner)
217
                            } else {
218
                                // The impossible futures failed because of a weak trace.
219
62
                                CounterExample::WeakTrace(trace)
220
                            }),
221
                        )
222
                    } else {
223
39
                        (result, None)
224
                    }
225
                }
226
            };
227

            
228
856
            trace!("Counter example tree: {:?}", ce_constructor);
229
856
            result
230
        } else {
231
            // Run without constructing a counter example.
232
9
            match refinement {
233
                RefinementType::Trace
234
                | RefinementType::Weaktrace
235
                | RefinementType::StableFailures
236
                | RefinementType::FailuresDivergences => {
237
7
                    let (result, _, _) =
238
7
                        is_failures_refinement(&merged_lts, initial_spec, refinement, strategy, &mut ());
239
7
                    (result, None)
240
                }
241
                RefinementType::ImpossibleFutures => {
242
2
                    let result = is_impossible_futures_refinement(&merged_lts, initial_spec, strategy, &mut ()).result;
243
2
                    (result, None)
244
                }
245
            }
246
        }
247
865
    })
248
1009
}
249

            
250
#[cfg(test)]
251
mod tests {
252
    use std::fs::File;
253
    use std::path::Path;
254
    use std::process::Command;
255

            
256
    use merc_io::DumpFiles;
257
    use merc_lts::mutate_lts;
258
    use merc_lts::random_lts;
259
    use merc_lts::write_mcrl2_aut;
260
    use merc_utilities::Timing;
261
    use merc_utilities::random_test;
262

            
263
    use crate::ExplorationStrategy;
264
    use crate::RefinementType;
265
    use crate::refines;
266

            
267
    #[test]
268
    #[cfg_attr(miri, ignore)] // Miri is too slow
269
1
    fn test_mcrl2_ltscompare_trace() {
270
1
        test_mcrl2_ltscompare_refinement("test_mcrl2_ltscompare_trace", RefinementType::Trace, "trace-ac");
271
1
    }
272

            
273
    #[test]
274
    #[cfg_attr(miri, ignore)] // Miri is too slow
275
1
    fn test_mcrl2_ltscompare_weak_trace() {
276
1
        test_mcrl2_ltscompare_refinement(
277
1
            "test_mcrl2_ltscompare_weak_trace",
278
1
            RefinementType::Weaktrace,
279
1
            "weak-trace-ac",
280
        );
281
1
    }
282

            
283
    #[test]
284
    #[cfg_attr(miri, ignore)] // Miri is too slow
285
1
    fn test_mcrl2_ltscompare_stable_failures() {
286
1
        test_mcrl2_ltscompare_refinement(
287
1
            "test_mcrl2_ltscompare_stable_failures",
288
1
            RefinementType::StableFailures,
289
1
            "weak-failures",
290
        );
291
1
    }
292

            
293
    #[test]
294
    #[cfg_attr(miri, ignore)] // Miri is too slow
295
1
    fn test_mcrl2_ltscompare_failures_divergences() {
296
1
        test_mcrl2_ltscompare_refinement(
297
1
            "test_mcrl2_ltscompare_failures_divergences",
298
1
            RefinementType::FailuresDivergences,
299
1
            "failures-divergence",
300
        );
301
1
    }
302

            
303
    #[test]
304
    #[cfg_attr(miri, ignore)] // Miri is too slow
305
1
    fn test_mcrl2_ltscompare_impossible_futures() {
306
1
        test_mcrl2_ltscompare_refinement(
307
1
            "test_mcrl2_ltscompare_impossible_futures",
308
1
            RefinementType::ImpossibleFutures,
309
1
            "impossible-futures",
310
        );
311
1
    }
312

            
313
    /// Compares our approach to the one implemented in mCRL2's ltscompare
314
5
    fn test_mcrl2_ltscompare_refinement(name: &str, refinement: RefinementType, argument: &str) {
315
5
        let Ok(mcrl2_path) = std::env::var("MCRL2_PATH") else {
316
5
            println!("Skipping test: MCRL2_PATH not set");
317
5
            return;
318
        };
319

            
320
        let ltscompare = Path::new(&mcrl2_path).join("ltscompare");
321

            
322
        // Write the random LTS to a temp file for ltscompare to process.
323
        let temp_dir = tempfile::tempdir().unwrap();
324
        let impl_path = temp_dir.path().join("impl.aut");
325
        let spec_path = temp_dir.path().join("spec.aut");
326

            
327
        random_test(100, |rng| {
328
            let files = DumpFiles::new(name);
329

            
330
            let spec_lts = random_lts::<String, _>(rng, 1000, 3);
331
            let impl_lts = mutate_lts(&spec_lts, rng, 100).unwrap();
332

            
333
            write_mcrl2_aut(&mut File::create(&impl_path).unwrap(), &impl_lts).unwrap();
334
            files
335
                .dump("impl.aut", |writer| write_mcrl2_aut(writer, &impl_lts))
336
                .unwrap();
337

            
338
            write_mcrl2_aut(&mut File::create(&spec_path).unwrap(), &spec_lts).unwrap();
339
            files
340
                .dump("spec.aut", |writer| write_mcrl2_aut(writer, &spec_lts))
341
                .unwrap();
342

            
343
            // Reduce the LTS using ltsconvert with branching bisimulation.
344
            let process = Command::new(&ltscompare)
345
                .arg(format!("-p{}", argument))
346
                .arg(&impl_path)
347
                .arg(&spec_path)
348
                .output()
349
                .expect("Failed to execute ltscompare");
350

            
351
            assert!(
352
                process.status.success(),
353
                "ltscompare failed with status: {}",
354
                process.status
355
            );
356

            
357
            let expected_result = String::from_utf8_lossy(&process.stdout).contains("true");
358
            let result = refines(
359
                impl_lts,
360
                spec_lts,
361
                refinement,
362
                ExplorationStrategy::BFS,
363
                false,
364
                false,
365
                &mut Timing::new(),
366
            )
367
            .0;
368

            
369
            // Check that the result is the same.
370
            assert_eq!(
371
                expected_result, result,
372
                "Mismatch between ltsconvert and our implementation"
373
            );
374
        });
375
5
    }
376
}