Skip to main content

rust_lstar/eqtest/
bdist_method.rs

1//! BDist equivalence-test implementation.
2//!
3//! The strategy compares state representatives and searches distinguishing
4//! suffixes up to a bounded depth.
5
6use crate::automata::Automata;
7use crate::eqtest::{Counterexample, EquivalenceTest};
8use crate::knowledge_base::KnowledgeBaseTrait;
9use crate::letter::Letter;
10use crate::query::OutputQuery;
11use crate::word::Word;
12use std::collections::{HashMap, VecDeque};
13use std::sync::{Arc, Mutex};
14
15/// BDist equivalence-test strategy.
16pub struct BDistMethod {
17    knowledge_base: Arc<Mutex<dyn KnowledgeBaseTrait>>,
18    input_letters: Vec<Letter>,
19    bdist: usize,
20}
21
22impl BDistMethod {
23    /// Create a new BDist equivalence tester.
24    ///
25    /// `bdist` controls the maximum distinguishing suffix length.
26    pub fn new(
27        knowledge_base: Arc<Mutex<dyn KnowledgeBaseTrait>>,
28        input_letters: Vec<Letter>,
29        bdist: usize,
30    ) -> Self {
31        Self {
32            knowledge_base,
33            input_letters,
34            bdist,
35        }
36    }
37
38    fn can_use_flat_transitions(hypothesis: &Automata) -> bool {
39        !hypothesis.transitions.is_empty()
40            && hypothesis
41                .transitions
42                .iter()
43                .all(|transition| !transition.source_state.is_empty())
44    }
45
46    fn outgoing_inputs(&self, hypothesis: &Automata, state_name: &str) -> Vec<(Letter, String)> {
47        if Self::can_use_flat_transitions(hypothesis) {
48            return hypothesis
49                .transitions
50                .iter()
51                .filter(|t| t.source_state == state_name)
52                .map(|t| (t.input_letter.clone(), t.output_state.name.clone()))
53                .collect();
54        }
55
56        let states = hypothesis.get_states();
57        if let Some(state) = states.iter().find(|s| s.name == state_name) {
58            return state
59                .transitions
60                .iter()
61                .map(|t| (t.input_letter.clone(), t.output_state.name.clone()))
62                .collect();
63        }
64        Vec::new()
65    }
66
67    fn compute_representatives(&self, hypothesis: &Automata) -> HashMap<String, Word> {
68        let mut representatives = HashMap::new();
69        let mut queue = VecDeque::new();
70        let initial = hypothesis.initial_state.name.clone();
71        representatives.insert(initial.clone(), Word::new());
72        queue.push_back(initial);
73
74        while let Some(state_name) = queue.pop_front() {
75            let rep_word = match representatives.get(&state_name) {
76                Some(w) => w.clone(),
77                None => continue,
78            };
79
80            for (input_letter, next_state_name) in self.outgoing_inputs(hypothesis, &state_name) {
81                if next_state_name == state_name || representatives.contains_key(&next_state_name) {
82                    continue;
83                }
84                let candidate = rep_word.concatenate(&Word::from_letters(vec![input_letter]));
85                representatives.insert(next_state_name.clone(), candidate);
86                queue.push_back(next_state_name);
87            }
88        }
89
90        representatives
91    }
92
93    fn generate_suffixes(&self) -> Vec<Word> {
94        fn generate_len(
95            letters: &[Letter],
96            len: usize,
97            current: &mut Vec<Letter>,
98            acc: &mut Vec<Word>,
99        ) {
100            if current.len() == len {
101                acc.push(Word::from_letters(current.clone()));
102                return;
103            }
104            for letter in letters {
105                current.push(letter.clone());
106                generate_len(letters, len, current, acc);
107                current.pop();
108            }
109        }
110
111        let mut suffixes = Vec::new();
112        for len in 1..=self.bdist {
113            let mut current = Vec::new();
114            generate_len(&self.input_letters, len, &mut current, &mut suffixes);
115        }
116        suffixes
117    }
118
119    fn check_equivalence(
120        &self,
121        w_i: &Word,
122        w_i_prime: &Word,
123        suffixes: &[Word],
124    ) -> Option<(Word, OutputQuery, OutputQuery)> {
125        for suffix in suffixes {
126            let mut query_i = OutputQuery::new(w_i.concatenate(suffix));
127            let mut query_i_prime = OutputQuery::new(w_i_prime.concatenate(suffix));
128
129            if self
130                .knowledge_base
131                .lock()
132                .unwrap()
133                .resolve_query(&mut query_i)
134                .is_err()
135            {
136                continue;
137            }
138            if self
139                .knowledge_base
140                .lock()
141                .unwrap()
142                .resolve_query(&mut query_i_prime)
143                .is_err()
144            {
145                continue;
146            }
147
148            let last_i = query_i.output_word().and_then(|w| w.last_letter()).cloned();
149            let last_i_prime = query_i_prime
150                .output_word()
151                .and_then(|w| w.last_letter())
152                .cloned();
153
154            if last_i != last_i_prime {
155                return Some((suffix.clone(), query_i, query_i_prime));
156            }
157        }
158        None
159    }
160}
161
162impl EquivalenceTest for BDistMethod {
163    fn find_counterexample(&self, hypothesis: &mut Automata) -> Option<Counterexample> {
164        let representatives = self.compute_representatives(hypothesis);
165        let suffixes = self.generate_suffixes();
166        let states = hypothesis.get_states();
167
168        for state in &states {
169            for letter in &self.input_letters {
170                let rep = match representatives.get(&state.name) {
171                    Some(w) => w.clone(),
172                    None => continue,
173                };
174                let w_i = rep.concatenate(&Word::from_letters(vec![letter.clone()]));
175                let one_letter_word = Word::from_letters(vec![letter.clone()]);
176
177                let (hyp_output_word, visited_states) =
178                    match hypothesis.play_word(&one_letter_word, Some(state)) {
179                        Ok(v) => v,
180                        Err(_) => continue,
181                    };
182
183                let mut query = OutputQuery::new(w_i.clone());
184                if self
185                    .knowledge_base
186                    .lock()
187                    .unwrap()
188                    .resolve_query(&mut query)
189                    .is_err()
190                {
191                    continue;
192                }
193
194                let hyp_last = hyp_output_word.last_letter().cloned();
195                let real_last = query.output_word().and_then(|w| w.last_letter()).cloned();
196                if hyp_last != real_last {
197                    return query
198                        .output_word()
199                        .cloned()
200                        .map(|output_word| Counterexample {
201                            input_word: query.input_word.clone(),
202                            output_word,
203                        });
204                }
205
206                let q_prime_name = match visited_states.last() {
207                    Some(s) => s.name.clone(),
208                    None => continue,
209                };
210                let w_i_prime = match representatives.get(&q_prime_name) {
211                    Some(w) => w,
212                    None => continue,
213                };
214                if &w_i == w_i_prime {
215                    continue;
216                }
217
218                if let Some((suffix, query_i, query_i_prime)) =
219                    self.check_equivalence(&w_i, w_i_prime, &suffixes)
220                {
221                    let w_i_suffix = w_i.concatenate(&suffix);
222                    let expected_i = match hypothesis.play_word(&w_i_suffix, None) {
223                        Ok((w, _)) => w,
224                        Err(_) => continue,
225                    };
226                    let observed_i = match query_i.output_word() {
227                        Some(w) => w.clone(),
228                        None => continue,
229                    };
230
231                    let chosen_query = if expected_i != observed_i {
232                        query_i
233                    } else {
234                        query_i_prime
235                    };
236
237                    if let Some(output_word) = chosen_query.output_word().cloned() {
238                        return Some(Counterexample {
239                            input_word: chosen_query.input_word.clone(),
240                            output_word,
241                        });
242                    }
243                }
244            }
245        }
246
247        None
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::automata::{State, Transition};
255
256    struct ConstantOutputKb {
257        output_symbol: String,
258    }
259
260    impl ConstantOutputKb {
261        fn new(output_symbol: &str) -> Self {
262            Self {
263                output_symbol: output_symbol.to_string(),
264            }
265        }
266    }
267
268    impl KnowledgeBaseTrait for ConstantOutputKb {
269        fn resolve_query(&mut self, query: &mut OutputQuery) -> Result<(), String> {
270            let outputs = query
271                .input_word
272                .letters()
273                .iter()
274                .map(|_| Letter::new(self.output_symbol.clone()))
275                .collect::<Vec<_>>();
276            query.set_result(Word::from_letters(outputs));
277            Ok(())
278        }
279
280        fn add_word(&mut self, _input_word: &Word, _output_word: &Word) -> Result<(), String> {
281            Ok(())
282        }
283    }
284
285    fn build_single_state_hypothesis(output_symbol: &str) -> Automata {
286        let mut s0 = State::new("S0".to_string());
287        s0.add_transition(Transition::new(
288            "t0".to_string(),
289            State::new("S0".to_string()),
290            Letter::new("a"),
291            Letter::new(output_symbol),
292        ));
293        Automata::new(s0, "H".to_string())
294    }
295
296    #[test]
297    fn bdist_finds_counterexample_for_wrong_hypothesis() {
298        let kb: Arc<Mutex<dyn KnowledgeBaseTrait>> =
299            Arc::new(Mutex::new(ConstantOutputKb::new("1")));
300        let eq = BDistMethod::new(kb, vec![Letter::new("a")], 2);
301        let mut hypothesis = build_single_state_hypothesis("0");
302        assert!(eq.find_counterexample(&mut hypothesis).is_some());
303    }
304
305    #[test]
306    fn bdist_returns_none_for_equivalent_hypothesis() {
307        let kb: Arc<Mutex<dyn KnowledgeBaseTrait>> =
308            Arc::new(Mutex::new(ConstantOutputKb::new("0")));
309        let eq = BDistMethod::new(kb, vec![Letter::new("a")], 2);
310        let mut hypothesis = build_single_state_hypothesis("0");
311        assert!(eq.find_counterexample(&mut hypothesis).is_none());
312    }
313}