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::InnerCe;
18
use crate::is_failures_refinement;
19
use crate::is_impossible_futures_refinement;
20

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

            
36
/// Determines the exploration strategy for the failures refinement algorithm.
37
///
38
/// Typically `BFS` is more suited for counter examples, but `DFS` can be more efficient
39
/// in practice when a counter example is not required.
40
#[derive(Clone, Copy)]
41
pub enum ExplorationStrategy {
42
    BFS,
43
    DFS,
44
}
45

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

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

            
91
100
                let impl_block = partition.block_number(preprocess_lts.initial_state_index());
92
100
                let spec_block = partition.block_number(initial_spec);
93

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

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

            
115
403
                let impl_block = partition.block_number(preprocess_lts.initial_state_index());
116
403
                let spec_block = partition.block_number(initial_spec);
117

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

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

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

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

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

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

            
202
179
                    if let Some(state) = ce_state {
203
                        // Reconstruct a trace from the counter example tree, relabelling the indices to their actual labels.
204
143
                        let trace = ce_constructor
205
143
                            .reconstruct_trace(state)
206
143
                            .iter()
207
157
                            .map(|l| merged_lts.labels()[*l].clone())
208
143
                            .collect();
209
                        (
210
143
                            result,
211
143
                            Some(if let Some(inner) = ce_inner {
212
75
                                CounterExample::ImpossibleFutures(trace, inner)
213
                            } else {
214
                                // The impossible futures failed because of a weak trace.
215
68
                                CounterExample::WeakTrace(trace)
216
                            }),
217
                        )
218
                    } else {
219
36
                        (result, None)
220
                    }
221
                }
222
            };
223

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

            
246
#[cfg(test)]
247
mod tests {
248
    use std::fs::File;
249
    use std::path::Path;
250
    use std::process::Command;
251

            
252
    use merc_io::DumpFiles;
253
    use merc_lts::mutate_lts;
254
    use merc_lts::random_lts;
255
    use merc_lts::write_mcrl2_aut;
256
    use merc_utilities::Timing;
257
    use merc_utilities::random_test;
258

            
259
    use crate::ExplorationStrategy;
260
    use crate::RefinementType;
261
    use crate::refines;
262

            
263
    #[test]
264
    #[cfg_attr(miri, ignore)] // Miri is too slow
265
1
    fn test_mcrl2_ltscompare_trace() {
266
1
        test_mcrl2_ltscompare_refinement("test_mcrl2_ltscompare_trace", RefinementType::Trace, "trace-ac");
267
1
    }
268

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

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

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

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

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

            
316
        let ltscompare = Path::new(&mcrl2_path).join("ltscompare");
317

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

            
323
        random_test(100, |rng| {
324
            let mut files = DumpFiles::new(name);
325

            
326
            let spec_lts = random_lts::<String, _>(rng, 1000, 3);
327
            let impl_lts = mutate_lts(&spec_lts, rng, 100).unwrap();
328

            
329
            write_mcrl2_aut(&mut File::create(&impl_path).unwrap(), &impl_lts).unwrap();
330
            files
331
                .dump("impl.aut", |writer| write_mcrl2_aut(writer, &impl_lts))
332
                .unwrap();
333

            
334
            write_mcrl2_aut(&mut File::create(&spec_path).unwrap(), &spec_lts).unwrap();
335
            files
336
                .dump("spec.aut", |writer| write_mcrl2_aut(writer, &spec_lts))
337
                .unwrap();
338

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

            
347
            assert!(
348
                process.status.success(),
349
                "ltscompare failed with status: {}",
350
                process.status
351
            );
352

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

            
365
            // Check that the result is the same.
366
            assert_eq!(
367
                expected_result, result,
368
                "Mismatch between ltsconvert and our implementation"
369
            );
370
        });
371
5
    }
372
}