1use std::collections::{btree_map::Entry, BTreeMap, BTreeSet};
10use std::error::Error;
11use std::fmt;
12
13use crate::grammar::{Grammar, GrammarExpr, GrammarFormat, GrammarParser, GrammarRule};
14use crate::{LinkNetwork, LinkType, ParseConfiguration, ParserRegistry};
15
16pub type Symbol = char;
18
19type Word = Vec<Symbol>;
20
21pub type ParserAcceptancePredicate = fn(&LinkNetwork, &str) -> bool;
23
24#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct Dfa {
27 pub alphabet: Vec<Symbol>,
29 pub states: usize,
31 pub start: usize,
33 pub accepting: Vec<bool>,
35 pub delta: Vec<Vec<usize>>,
37}
38
39impl Dfa {
40 #[must_use]
42 pub fn accepts(&self, word: &[Symbol]) -> bool {
43 let mut state = self.start;
44 if state >= self.states {
45 return false;
46 }
47
48 for symbol in word {
49 let Some(symbol_index) = self.symbol_index(*symbol) else {
50 return false;
51 };
52 let Some(next) = self
53 .delta
54 .get(state)
55 .and_then(|row| row.get(symbol_index))
56 .copied()
57 else {
58 return false;
59 };
60 if next >= self.states {
61 return false;
62 }
63 state = next;
64 }
65
66 self.accepting.get(state).copied().unwrap_or(false)
67 }
68
69 #[must_use]
71 pub fn accepts_text(&self, text: &str) -> bool {
72 self.accepts(&text.chars().collect::<Vec<_>>())
73 }
74
75 #[must_use]
77 pub fn to_grammar(&self) -> Grammar {
78 let mut grammar = Grammar::new().with_source_format(GrammarFormat::Inferred);
79 if self.states == 0 || self.start >= self.states {
80 return grammar;
81 }
82
83 for state in 0..self.states {
84 grammar.add_rule(GrammarRule::new(
85 state_name(state),
86 self.state_expression(state),
87 ));
88 }
89 grammar.set_start(state_name(self.start));
90 grammar
91 }
92
93 fn state_expression(&self, state: usize) -> GrammarExpr {
94 let mut alternatives = Vec::new();
95
96 if self.accepting.get(state).copied().unwrap_or(false) {
97 alternatives.push(GrammarExpr::Empty);
98 }
99
100 if let Some(transitions) = self.delta.get(state) {
101 for (symbol_index, target) in transitions.iter().copied().enumerate() {
102 if target >= self.states {
103 continue;
104 }
105 let Some(symbol) = self.alphabet.get(symbol_index) else {
106 continue;
107 };
108 alternatives.push(GrammarExpr::Sequence(vec![
109 GrammarExpr::Terminal(symbol.to_string()),
110 GrammarExpr::NonTerminal(state_name(target)),
111 ]));
112 }
113 }
114
115 match alternatives.as_slice() {
116 [only] => only.clone(),
117 _ => GrammarExpr::Choice {
118 ordered: false,
119 alternatives,
120 },
121 }
122 }
123
124 fn symbol_index(&self, symbol: Symbol) -> Option<usize> {
125 self.alphabet
126 .iter()
127 .position(|candidate| *candidate == symbol)
128 }
129
130 fn validate(&self) -> Result<(), ActiveLearningError> {
131 validate_alphabet(&self.alphabet)?;
132 if self.start >= self.states {
133 return Err(ActiveLearningError::InvalidDfa {
134 reason: format!(
135 "start state {} is outside {} states",
136 self.start, self.states
137 ),
138 });
139 }
140 if self.accepting.len() != self.states {
141 return Err(ActiveLearningError::InvalidDfa {
142 reason: format!(
143 "accepting vector has {} entries for {} states",
144 self.accepting.len(),
145 self.states
146 ),
147 });
148 }
149 if self.delta.len() != self.states {
150 return Err(ActiveLearningError::InvalidDfa {
151 reason: format!(
152 "transition table has {} rows for {} states",
153 self.delta.len(),
154 self.states
155 ),
156 });
157 }
158 for (state, transitions) in self.delta.iter().enumerate() {
159 if transitions.len() != self.alphabet.len() {
160 return Err(ActiveLearningError::InvalidDfa {
161 reason: format!(
162 "state {state} has {} transitions for {} symbols",
163 transitions.len(),
164 self.alphabet.len()
165 ),
166 });
167 }
168 if let Some(target) = transitions.iter().find(|target| **target >= self.states) {
169 return Err(ActiveLearningError::InvalidDfa {
170 reason: format!("state {state} transitions to invalid state {target}"),
171 });
172 }
173 }
174 Ok(())
175 }
176}
177
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
180pub struct ActiveLearningConfig {
181 pub max_word_len: usize,
183 pub equivalence_samples: usize,
185 pub seed: u64,
187 pub use_ttt: bool,
189 pub max_iterations: usize,
191}
192
193impl Default for ActiveLearningConfig {
194 fn default() -> Self {
195 Self {
196 max_word_len: 8,
197 equivalence_samples: 256,
198 seed: 0xA17E_1EAF_DFA5_EED5,
199 use_ttt: false,
200 max_iterations: 128,
201 }
202 }
203}
204
205#[derive(Clone, Debug, PartialEq, Eq)]
207pub enum ActiveLearningError {
208 DuplicateSymbol {
210 symbol: Symbol,
212 },
213 TttUnavailable,
215 InvalidConfig {
217 reason: String,
219 },
220 InvalidDfa {
222 reason: String,
224 },
225 MaxIterations {
227 max_iterations: usize,
229 },
230}
231
232impl fmt::Display for ActiveLearningError {
233 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
234 match self {
235 Self::DuplicateSymbol { symbol } => {
236 write!(formatter, "alphabet contains duplicate symbol {symbol:?}")
237 }
238 Self::TttUnavailable => formatter.write_str("TTT active learning is not implemented"),
239 Self::InvalidConfig { reason } | Self::InvalidDfa { reason } => {
240 formatter.write_str(reason)
241 }
242 Self::MaxIterations { max_iterations } => write!(
243 formatter,
244 "active learner did not converge within {max_iterations} iterations"
245 ),
246 }
247 }
248}
249
250impl Error for ActiveLearningError {}
251
252pub trait Oracle {
254 fn alphabet(&self) -> &[Symbol];
256
257 fn membership(&self, word: &[Symbol]) -> bool;
259
260 fn equivalence(&self, hypothesis: &Dfa, config: &ActiveLearningConfig) -> Option<Word>;
266}
267
268#[derive(Clone, Debug)]
270pub struct SamplingEquivalenceOracle<M> {
271 alphabet: Vec<Symbol>,
272 membership: M,
273}
274
275impl<M> SamplingEquivalenceOracle<M> {
276 #[must_use]
278 pub const fn new(alphabet: Vec<Symbol>, membership: M) -> Self {
279 Self {
280 alphabet,
281 membership,
282 }
283 }
284}
285
286impl<M> Oracle for SamplingEquivalenceOracle<M>
287where
288 M: Fn(&[Symbol]) -> bool,
289{
290 fn alphabet(&self) -> &[Symbol] {
291 &self.alphabet
292 }
293
294 fn membership(&self, word: &[Symbol]) -> bool {
295 (self.membership)(word)
296 }
297
298 fn equivalence(&self, hypothesis: &Dfa, config: &ActiveLearningConfig) -> Option<Word> {
299 sampled_counterexample(hypothesis, &self.alphabet, config, |word| {
300 self.membership(word)
301 })
302 }
303}
304
305#[derive(Clone, Debug)]
307pub struct GrammarAcceptorOracle {
308 parser: GrammarParser,
309 alphabet: Vec<Symbol>,
310}
311
312impl GrammarAcceptorOracle {
313 #[must_use]
315 pub fn new(grammar: Grammar, alphabet: Vec<Symbol>) -> Self {
316 Self {
317 parser: GrammarParser::new(grammar),
318 alphabet,
319 }
320 }
321}
322
323impl Oracle for GrammarAcceptorOracle {
324 fn alphabet(&self) -> &[Symbol] {
325 &self.alphabet
326 }
327
328 fn membership(&self, word: &[Symbol]) -> bool {
329 self.parser.accepts(&word_text(word))
330 }
331
332 fn equivalence(&self, hypothesis: &Dfa, config: &ActiveLearningConfig) -> Option<Word> {
333 sampled_counterexample(hypothesis, &self.alphabet, config, |word| {
334 self.membership(word)
335 })
336 }
337}
338
339#[derive(Clone, Debug)]
341pub struct ParserMembershipOracle {
342 registry: ParserRegistry,
343 language: String,
344 alphabet: Vec<Symbol>,
345 configuration: ParseConfiguration,
346 acceptance: ParserAcceptancePredicate,
347}
348
349impl ParserMembershipOracle {
350 #[must_use]
352 pub fn new(
353 registry: ParserRegistry,
354 language: impl Into<String>,
355 alphabet: Vec<Symbol>,
356 ) -> Self {
357 Self {
358 registry,
359 language: language.into(),
360 alphabet,
361 configuration: ParseConfiguration::default(),
362 acceptance: clean_structural_acceptance,
363 }
364 }
365
366 #[must_use]
368 pub const fn with_configuration(mut self, configuration: ParseConfiguration) -> Self {
369 self.configuration = configuration;
370 self
371 }
372
373 #[must_use]
375 pub const fn with_acceptance_predicate(
376 mut self,
377 acceptance: ParserAcceptancePredicate,
378 ) -> Self {
379 self.acceptance = acceptance;
380 self
381 }
382}
383
384impl Oracle for ParserMembershipOracle {
385 fn alphabet(&self) -> &[Symbol] {
386 &self.alphabet
387 }
388
389 fn membership(&self, word: &[Symbol]) -> bool {
390 let text = word_text(word);
391 let network = self
392 .registry
393 .parse(&text, &self.language, self.configuration);
394 (self.acceptance)(&network, &text)
395 }
396
397 fn equivalence(&self, hypothesis: &Dfa, config: &ActiveLearningConfig) -> Option<Word> {
398 sampled_counterexample(hypothesis, &self.alphabet, config, |word| {
399 self.membership(word)
400 })
401 }
402}
403
404#[must_use]
412pub fn clean_structural_acceptance(network: &LinkNetwork, text: &str) -> bool {
413 network.reconstruct_text() == text
414 && network.verify_full_match(None).is_clean()
415 && network.links().any(|link| {
416 link.metadata().span().is_some()
417 && matches!(
418 link.metadata().link_type(),
419 Some(LinkType::Grammar | LinkType::Syntax)
420 )
421 })
422}
423
424pub fn learn_dfa(
426 oracle: &dyn Oracle,
427 config: &ActiveLearningConfig,
428) -> Result<Dfa, ActiveLearningError> {
429 validate_config(config)?;
430 validate_alphabet(oracle.alphabet())?;
431 if config.use_ttt {
432 return Err(ActiveLearningError::TttUnavailable);
433 }
434
435 let mut table = ObservationTable::new(oracle);
436 for _ in 0..config.max_iterations {
437 table.close_and_consistent();
438 let hypothesis = table.hypothesis()?;
439 hypothesis.validate()?;
440
441 if let Some(counterexample) = oracle.equivalence(&hypothesis, config) {
442 table.add_counterexample(&counterexample);
443 } else {
444 return Ok(hypothesis);
445 }
446 }
447
448 Err(ActiveLearningError::MaxIterations {
449 max_iterations: config.max_iterations,
450 })
451}
452
453pub fn learn_grammar(
455 oracle: &dyn Oracle,
456 config: &ActiveLearningConfig,
457) -> Result<Grammar, ActiveLearningError> {
458 learn_dfa(oracle, config).map(|dfa| dfa.to_grammar())
459}
460
461struct ObservationTable<'oracle> {
462 oracle: &'oracle dyn Oracle,
463 alphabet: Vec<Symbol>,
464 prefixes: Vec<Word>,
465 suffixes: Vec<Word>,
466 table: BTreeMap<(Word, Word), bool>,
467}
468
469impl<'oracle> ObservationTable<'oracle> {
470 fn new(oracle: &'oracle dyn Oracle) -> Self {
471 Self {
472 oracle,
473 alphabet: oracle.alphabet().to_vec(),
474 prefixes: vec![Vec::new()],
475 suffixes: vec![Vec::new()],
476 table: BTreeMap::new(),
477 }
478 }
479
480 fn close_and_consistent(&mut self) {
481 loop {
482 self.fill();
483 if let Some(prefix) = self.unclosed_prefix() {
484 self.add_prefix_closure(&prefix);
485 continue;
486 }
487 if let Some(suffix) = self.inconsistent_suffix() {
488 self.add_suffix_closure(&suffix);
489 continue;
490 }
491 break;
492 }
493 }
494
495 fn hypothesis(&mut self) -> Result<Dfa, ActiveLearningError> {
496 self.fill();
497
498 let prefixes = self.prefixes.clone();
499 let mut signature_states = BTreeMap::new();
500 let mut representatives = Vec::new();
501 for prefix in prefixes {
502 let signature = self.row(&prefix);
503 if let Entry::Vacant(entry) = signature_states.entry(signature) {
504 let state = representatives.len();
505 entry.insert(state);
506 representatives.push(prefix);
507 }
508 }
509
510 let states = representatives.len();
511 let start_signature = self.row(&[]);
512 let start = signature_states
513 .get(&start_signature)
514 .copied()
515 .ok_or_else(|| ActiveLearningError::InvalidDfa {
516 reason: "observation table has no start row".to_string(),
517 })?;
518
519 let mut accepting = vec![false; states];
520 let mut delta = vec![vec![0; self.alphabet.len()]; states];
521 for (state, representative) in representatives.iter().enumerate() {
522 accepting[state] = self.value(representative, &[]);
523 for (symbol_index, symbol) in self.alphabet.clone().into_iter().enumerate() {
524 let successor = extend(representative, symbol);
525 let signature = self.row(&successor);
526 let target = signature_states.get(&signature).copied().ok_or_else(|| {
527 ActiveLearningError::InvalidDfa {
528 reason: "closed table did not contain a successor row".to_string(),
529 }
530 })?;
531 delta[state][symbol_index] = target;
532 }
533 }
534
535 Ok(Dfa {
536 alphabet: self.alphabet.clone(),
537 states,
538 start,
539 accepting,
540 delta,
541 })
542 }
543
544 fn fill(&mut self) {
545 let mut rows = self.prefixes.clone();
546 rows.extend(self.lower_prefixes());
547 let suffixes = self.suffixes.clone();
548 for row in rows {
549 for suffix in &suffixes {
550 self.value(&row, suffix);
551 }
552 }
553 }
554
555 fn unclosed_prefix(&mut self) -> Option<Word> {
556 let upper_signatures = self
557 .prefixes
558 .clone()
559 .into_iter()
560 .map(|prefix| self.row(&prefix))
561 .collect::<BTreeSet<_>>();
562
563 self.lower_prefixes()
564 .into_iter()
565 .find(|prefix| !upper_signatures.contains(&self.row(prefix)))
566 }
567
568 fn inconsistent_suffix(&mut self) -> Option<Word> {
569 let prefixes = self.prefixes.clone();
570 for left_index in 0..prefixes.len() {
571 for right_index in (left_index + 1)..prefixes.len() {
572 let left = &prefixes[left_index];
573 let right = &prefixes[right_index];
574 if self.row(left) != self.row(right) {
575 continue;
576 }
577 for symbol in self.alphabet.clone() {
578 let left_successor = extend(left, symbol);
579 let right_successor = extend(right, symbol);
580 if self.row(&left_successor) == self.row(&right_successor) {
581 continue;
582 }
583 for suffix in self.suffixes.clone() {
584 if self.value(&left_successor, &suffix)
585 != self.value(&right_successor, &suffix)
586 {
587 let mut distinguishing = vec![symbol];
588 distinguishing.extend(suffix);
589 return Some(distinguishing);
590 }
591 }
592 }
593 }
594 }
595 None
596 }
597
598 fn lower_prefixes(&self) -> Vec<Word> {
599 let known = self.prefixes.iter().cloned().collect::<BTreeSet<_>>();
600 let mut lower = Vec::new();
601 for prefix in &self.prefixes {
602 for symbol in &self.alphabet {
603 let word = extend(prefix, *symbol);
604 if !known.contains(&word) {
605 lower.push(word);
606 }
607 }
608 }
609 lower
610 }
611
612 fn row(&mut self, prefix: &[Symbol]) -> Vec<bool> {
613 self.suffixes
614 .clone()
615 .into_iter()
616 .map(|suffix| self.value(prefix, &suffix))
617 .collect()
618 }
619
620 fn value(&mut self, prefix: &[Symbol], suffix: &[Symbol]) -> bool {
621 let key = (prefix.to_vec(), suffix.to_vec());
622 if let Some(value) = self.table.get(&key) {
623 return *value;
624 }
625
626 let mut word = prefix.to_vec();
627 word.extend(suffix);
628 let value = self.oracle.membership(&word);
629 self.table.insert(key, value);
630 value
631 }
632
633 fn add_counterexample(&mut self, word: &[Symbol]) {
634 for length in 0..=word.len() {
635 self.add_prefix(&word[..length]);
636 }
637 }
638
639 fn add_prefix_closure(&mut self, word: &[Symbol]) {
640 for length in 0..=word.len() {
641 self.add_prefix(&word[..length]);
642 }
643 }
644
645 fn add_prefix(&mut self, word: &[Symbol]) {
646 if !self.prefixes.iter().any(|prefix| prefix == word) {
647 self.prefixes.push(word.to_vec());
648 }
649 }
650
651 fn add_suffix_closure(&mut self, suffix: &[Symbol]) {
652 for start in 0..=suffix.len() {
653 self.add_suffix(&suffix[start..]);
654 }
655 }
656
657 fn add_suffix(&mut self, suffix: &[Symbol]) {
658 if !self.suffixes.iter().any(|candidate| candidate == suffix) {
659 self.suffixes.push(suffix.to_vec());
660 }
661 }
662}
663
664fn sampled_counterexample<F>(
665 hypothesis: &Dfa,
666 alphabet: &[Symbol],
667 config: &ActiveLearningConfig,
668 membership: F,
669) -> Option<Word>
670where
671 F: Fn(&[Symbol]) -> bool,
672{
673 sample_words(
674 alphabet,
675 config.max_word_len,
676 config.equivalence_samples,
677 config.seed,
678 )
679 .into_iter()
680 .find(|word| hypothesis.accepts(word) != membership(word))
681}
682
683fn sample_words(
684 alphabet: &[Symbol],
685 max_word_len: usize,
686 equivalence_samples: usize,
687 seed: u64,
688) -> Vec<Word> {
689 if equivalence_samples == 0 {
690 return Vec::new();
691 }
692
693 let mut words = Vec::with_capacity(equivalence_samples);
694 push_unique(&mut words, Vec::new(), equivalence_samples);
695
696 for length in 1..=max_word_len {
697 for symbol in alphabet {
698 push_unique(&mut words, vec![*symbol; length], equivalence_samples);
699 }
700 }
701
702 if alphabet.is_empty() || words.len() >= equivalence_samples {
703 words.truncate(equivalence_samples);
704 return words;
705 }
706
707 let mut rng = SplitMix64::new(seed);
708 let random_target = equivalence_samples.saturating_sub(words.len()) / 2;
709 let mut random_added = 0usize;
710 let mut attempts = 0usize;
711 while random_added < random_target && attempts < equivalence_samples.saturating_mul(32) {
712 attempts += 1;
713 let length = rng.next_usize(max_word_len.saturating_add(1));
714 let mut word = Vec::with_capacity(length);
715 for _ in 0..length {
716 word.push(alphabet[rng.next_usize(alphabet.len())]);
717 }
718 let before = words.len();
719 push_unique(&mut words, word, equivalence_samples);
720 if words.len() > before {
721 random_added += 1;
722 }
723 }
724
725 fill_exhaustive(alphabet, max_word_len, equivalence_samples, &mut words);
726 words.truncate(equivalence_samples);
727 words
728}
729
730fn fill_exhaustive(alphabet: &[Symbol], max_word_len: usize, limit: usize, words: &mut Vec<Word>) {
731 let mut current = vec![Vec::new()];
732 for _ in 0..max_word_len {
733 let mut next = Vec::new();
734 for prefix in ¤t {
735 for symbol in alphabet {
736 let word = extend(prefix, *symbol);
737 push_unique(words, word.clone(), limit);
738 next.push(word);
739 if words.len() >= limit {
740 return;
741 }
742 }
743 }
744 current = next;
745 }
746}
747
748fn push_unique(words: &mut Vec<Word>, word: Word, limit: usize) {
749 if words.len() < limit && !words.contains(&word) {
750 words.push(word);
751 }
752}
753
754fn validate_config(config: &ActiveLearningConfig) -> Result<(), ActiveLearningError> {
755 if config.max_iterations == 0 {
756 return Err(ActiveLearningError::InvalidConfig {
757 reason: "max_iterations must be greater than zero".to_string(),
758 });
759 }
760 Ok(())
761}
762
763fn validate_alphabet(alphabet: &[Symbol]) -> Result<(), ActiveLearningError> {
764 let mut seen = BTreeSet::new();
765 for symbol in alphabet {
766 if !seen.insert(*symbol) {
767 return Err(ActiveLearningError::DuplicateSymbol { symbol: *symbol });
768 }
769 }
770 Ok(())
771}
772
773fn extend(prefix: &[Symbol], symbol: Symbol) -> Word {
774 let mut word = prefix.to_vec();
775 word.push(symbol);
776 word
777}
778
779fn word_text(word: &[Symbol]) -> String {
780 word.iter().collect()
781}
782
783fn state_name(state: usize) -> String {
784 format!("q{state}")
785}
786
787#[derive(Clone, Copy, Debug)]
788struct SplitMix64 {
789 state: u64,
790}
791
792impl SplitMix64 {
793 const fn new(seed: u64) -> Self {
794 Self { state: seed }
795 }
796
797 fn next_u64(&mut self) -> u64 {
798 self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
799 let mut value = self.state;
800 value = (value ^ (value >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
801 value = (value ^ (value >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
802 value ^ (value >> 31)
803 }
804
805 fn next_usize(&mut self, upper: usize) -> usize {
806 if upper == 0 {
807 return 0;
808 }
809 let upper = u64::try_from(upper).unwrap_or(u64::MAX);
810 usize::try_from(self.next_u64() % upper).unwrap_or(0)
811 }
812}