1
#![forbid(unsafe_code)]
2

            
3
use log::debug;
4
use log::trace;
5
use merc_io::LargeFormatter;
6
use merc_lts::LTS;
7
use merc_lts::LabelIndex;
8
use merc_lts::StateIndex;
9

            
10
use crate::BlockIndex;
11
use crate::IndexedPartition;
12
use crate::Partition;
13
use crate::sort_topological;
14

            
15
/// Computes the strongly connected tau component partitioning of the given LTS.
16
801
pub fn tau_scc_decomposition(lts: &impl LTS) -> IndexedPartition {
17
16385
    scc_decomposition(lts, &|_, label_index, _| lts.is_hidden_label(label_index))
18
801
}
19

            
20
/// Computes the strongly connected component partitioning of the given LTS.
21
801
pub fn scc_decomposition<F>(lts: &impl LTS, filter: &F) -> IndexedPartition
22
801
where
23
801
    F: Fn(StateIndex, LabelIndex, StateIndex) -> bool,
24
{
25
801
    let mut partition = IndexedPartition::new(lts.num_of_states());
26

            
27
    // The stack for the depth first search.
28
801
    let mut stack = Vec::new();
29

            
30
    // Keep track of already visited states.
31
801
    let mut state_info: Vec<Option<StateInfo>> = vec![None; lts.num_of_states()];
32

            
33
801
    let mut smallest_index = 0;
34
801
    let mut next_block_number = BlockIndex::new(0);
35

            
36
    // The outer depth first search used to traverse all the states.
37
7783
    for state_index in lts.iter_states() {
38
7783
        if state_info[state_index].is_none() {
39
4151
            trace!("State {state_index}");
40

            
41
4151
            strongly_connect(
42
4151
                state_index,
43
4151
                lts,
44
4151
                filter,
45
4151
                &mut partition,
46
4151
                &mut smallest_index,
47
4151
                &mut next_block_number,
48
4151
                &mut stack,
49
4151
                &mut state_info,
50
            )
51
3632
        }
52
    }
53

            
54
801
    trace!("SCC partition {partition}");
55
801
    debug!(
56
        "Found {} strongly connected components",
57
        LargeFormatter(partition.num_of_blocks())
58
    );
59
801
    partition
60
801
}
61

            
62
#[derive(Clone, Debug)]
63
struct StateInfo {
64
    /// A unique index for every state.
65
    index: usize,
66

            
67
    /// Keeps track of the lowest state that can be reached on the stack.
68
    lowlink: usize,
69

            
70
    /// Keeps track of whether this state is on the stack.
71
    on_stack: bool,
72
}
73

            
74
/// Tarjan's strongly connected components algorithm.
75
///
76
/// The `filter` can be used to determine which (from, label, to) edges should
77
/// to be connected.
78
///
79
/// The `smallest_index`, `stack` and `indices` are updated in each recursive
80
/// call to keep track of the current SCC.
81
#[allow(clippy::too_many_arguments)]
82
7783
fn strongly_connect<F>(
83
7783
    state_index: StateIndex,
84
7783
    lts: &impl LTS,
85
7783
    filter: &F,
86
7783
    partition: &mut IndexedPartition,
87
7783
    smallest_index: &mut usize,
88
7783
    next_block_number: &mut BlockIndex,
89
7783
    stack: &mut Vec<StateIndex>,
90
7783
    state_info: &mut Vec<Option<StateInfo>>,
91
7783
) where
92
7783
    F: Fn(StateIndex, LabelIndex, StateIndex) -> bool,
93
{
94
7783
    trace!("Visiting state {state_index}");
95

            
96
7783
    state_info[state_index] = Some(StateInfo {
97
7783
        index: *smallest_index,
98
7783
        lowlink: *smallest_index,
99
7783
        on_stack: true,
100
7783
    });
101

            
102
7783
    *smallest_index += 1;
103

            
104
    // Start a depth first search from the current state.
105
7783
    stack.push(state_index);
106

            
107
    // Consider successors of the current state.
108
16385
    for transition in lts.outgoing_transitions(state_index) {
109
16385
        if filter(state_index, transition.label, transition.to) {
110
7372
            if let Some(meta) = &mut state_info[transition.to] {
111
3740
                if meta.on_stack {
112
940
                    // Successor w is in stack S and hence in the current SCC
113
940
                    // If w is not on stack, then (v, w) is an edge pointing to an SCC already found and must be ignored
114
940
                    // v.lowlink := min(v.lowlink, w.lowlink);
115
940
                    let w_index = state_info[transition.to]
116
940
                        .as_ref()
117
940
                        .expect("The state must be visited in the recursive call")
118
940
                        .index;
119
940
                    let info = state_info[state_index.value()]
120
940
                        .as_mut()
121
940
                        .expect("This state was added before");
122
940
                    info.lowlink = info.lowlink.min(w_index);
123
2800
                }
124
3632
            } else {
125
3632
                // Successor w has not yet been visited; recurse on it
126
3632
                strongly_connect(
127
3632
                    transition.to,
128
3632
                    lts,
129
3632
                    filter,
130
3632
                    partition,
131
3632
                    smallest_index,
132
3632
                    next_block_number,
133
3632
                    stack,
134
3632
                    state_info,
135
3632
                );
136
3632

            
137
3632
                // v.lowlink := min(v.lowlink, w.lowlink);
138
3632
                let w_lowlink = state_info[transition.to.value()]
139
3632
                    .as_ref()
140
3632
                    .expect("The state must be visited in the recursive call")
141
3632
                    .lowlink;
142
3632
                let info = state_info[state_index.value()]
143
3632
                    .as_mut()
144
3632
                    .expect("This state was added before");
145
3632
                info.lowlink = info.lowlink.min(w_lowlink);
146
3632
            }
147
9013
        }
148
    }
149

            
150
7783
    let info = state_info[state_index.value()]
151
7783
        .as_ref()
152
7783
        .expect("This state was added before");
153
7783
    if info.lowlink == info.index {
154
        // Start a new strongly connected component.
155
7783
        while let Some(index) = stack.pop() {
156
7783
            let info = state_info[index.value()].as_mut().expect("This state was on the stack");
157
7783
            info.on_stack = false;
158

            
159
7783
            trace!("Added state {index} to block {next_block_number}");
160
7783
            partition.set_block(index, *next_block_number);
161

            
162
7783
            if index == state_index || stack.is_empty() {
163
7389
                *next_block_number = BlockIndex::new(next_block_number.value() + 1);
164
7389
                break;
165
394
            }
166
        }
167
394
    }
168
7783
}
169

            
170
/// Returns true iff the labelled transition system has tau-loops.
171
100
pub fn has_tau_loop<L>(lts: &L) -> bool
172
100
where
173
100
    L: LTS,
174
{
175
3384
    sort_topological(lts, |label_index, _| lts.is_hidden_label(label_index), false).is_err()
176
100
}
177

            
178
#[cfg(test)]
179
mod tests {
180
    use merc_io::DumpFiles;
181
    use merc_lts::LabelIndex;
182
    use merc_lts::LabelledTransitionSystem;
183
    use merc_lts::StateIndex;
184
    use merc_lts::random_lts;
185
    use merc_lts::write_aut;
186
    use merc_utilities::random_test;
187
    use test_log::test;
188

            
189
    use crate::Partition;
190
    use crate::quotient_lts_naive;
191

            
192
    use super::*;
193

            
194
    /// Returns the reachable states from the given state index.
195
988
    fn reachable_states(
196
988
        lts: &impl LTS,
197
988
        state_index: StateIndex,
198
988
        filter: &impl Fn(StateIndex, LabelIndex, StateIndex) -> bool,
199
988
    ) -> Vec<usize> {
200
988
        let mut stack = vec![state_index];
201
988
        let mut visited = vec![false; lts.num_of_states()];
202

            
203
        // Depth first search to find all reachable states.
204
14516
        while let Some(inner_state_index) = stack.pop() {
205
29519
            for transition in lts.outgoing_transitions(inner_state_index) {
206
29519
                if filter(inner_state_index, LabelIndex::new(0), transition.to) && !visited[transition.to.value()] {
207
12540
                    visited[transition.to.value()] = true;
208
12540
                    stack.push(transition.to);
209
16979
                }
210
            }
211
        }
212

            
213
        // All the states that were visited are reachable.
214
988
        visited
215
988
            .into_iter()
216
988
            .enumerate()
217
34614
            .filter_map(|(index, visited)| if visited { Some(index) } else { None })
218
988
            .collect()
219
988
    }
220

            
221
    #[test]
222
    #[cfg_attr(miri, ignore)]
223
    fn test_random_tau_scc_decomposition() {
224
100
        random_test(100, |rng| {
225
100
            let mut files = DumpFiles::new("test_random_tau_scc_decomposition");
226

            
227
100
            let lts = random_lts(rng, 10, 3, 3);
228
100
            files.dump("input.aut", |f| write_aut(f, &lts)).unwrap();
229

            
230
100
            let partitioning = tau_scc_decomposition(&lts);
231
100
            let reduction = quotient_lts_naive(&lts, &partitioning, true);
232
100
            assert!(!has_tau_loop(&reduction), "The SCC decomposition contains tau-loops");
233

            
234
100
            files.dump("tau_scc_decomposition.aut", |f| write_aut(f, &reduction)).unwrap();
235

            
236
            // Check that states in a strongly connected component are reachable from each other.
237
988
            for state_index in lts.iter_states() {
238
29519
                let reachable = reachable_states(&lts, state_index, &|_, label, _| lts.is_hidden_label(label));
239

            
240
                // All other states in the same block should be reachable.
241
988
                let block = partitioning.block_number(state_index);
242

            
243
988
                for other_state_index in lts
244
988
                    .iter_states()
245
34614
                    .filter(|index| state_index != *index && partitioning.block_number(*index) == block)
246
                {
247
156
                    assert!(
248
156
                        reachable.contains(&other_state_index),
249
                        "State {state_index} and {other_state_index} should be connected"
250
                    );
251
                }
252
            }
253

            
254
100
            assert!(
255
100
                reduction.num_of_states() == tau_scc_decomposition(&reduction).num_of_blocks(),
256
                "Applying SCC decomposition again should yield the same number of SCC after second application"
257
            );
258
100
        });
259
    }
260

            
261
    #[test]
262
    fn test_cycles() {
263
        let transitions = [(0, 0, 2), (0, 0, 4), (1, 0, 0), (2, 0, 1), (2, 0, 0)]
264
5
            .map(|(from, label, to)| (StateIndex::new(from), LabelIndex::new(label), StateIndex::new(to)));
265

            
266
        let lts = LabelledTransitionSystem::new(
267
            StateIndex::new(0),
268
            None,
269
2
            || transitions.iter().cloned(),
270
            vec!["tau".to_string(), "a".to_string()],
271
        );
272

            
273
        let _ = tau_scc_decomposition(&lts);
274
    }
275
}