diff --git a/automata/src/combinators.rs b/automata/src/combinators.rs index ebf1aec..7b406e3 100644 --- a/automata/src/combinators.rs +++ b/automata/src/combinators.rs @@ -128,6 +128,76 @@ impl ops::Shr for Deterministic { } } +impl ops::BitXor for Deterministic { + type Output = Self; + #[inline] + fn bitxor(mut self, other: Self) -> Self::Output { + let rhs_init = get!(other.states, other.initial) + .transitions + .clone() + .generalize(); + + let accepting_indices = + self.states + .iter_mut() + .enumerate() + .fold(BTreeSet::new(), |mut acc_i, (i, st)| { + if st.non_accepting.is_empty() { + st.non_accepting = iter::once( + "Ran the first part of a two-parser call \ + (with `^`) but not the second one." + .to_owned(), + ) + .collect(); // <-- No longer accepting since we need to run the second parser + let _ = acc_i.insert(i); + } + acc_i + }); + + let mut s = self.generalize(); + if s.check().is_err() { + panic!("Internal error") + } + let size = s.states.len(); + + let Graph { + states: other_states, + initial: other_initial, + } = other + .generalize() + .map_indices(|i| i.checked_add(size).expect("Absurdly huge number of states")); + + s.states.extend(other_states); + + // For every transition that an empty stack can take from the initial state of the right-hand parser, + // add that transition (only on the empty stack) to each accepting state of the left-hand parser. + for state in &mut s.states { + state.transitions = mem::replace( + &mut state.transitions, + Curry::Wildcard(Transition::Return { region: "" }), + ) + .merge(rhs_init.clone()) + .unwrap_or_else(|e| panic!("{e}")); + } + + // If any initial states are immediately accepting, we need to start in the second parser, too. + if s.initial.iter().any(|i| accepting_indices.contains(i)) { + s.initial.extend(other_initial.iter().copied()); + } + + let mut out = Graph { + states: s + .states + .into_iter() + .map(|st| add_call_state(st, &other_initial, &accepting_indices)) + .collect(), + ..s + }; + out.sort(); + out.determinize().unwrap_or_else(|e| panic!("{e}")) + } +} + /// Add a tail call to any accepting state. #[inline] #[must_use] @@ -225,3 +295,92 @@ fn add_tail_call_c>( iter.collect() } } + +/// Add a call to any accepting state. +#[inline] +#[must_use] +fn add_call_state>( + s: State, + other_init: &BTreeSet, + accepting_indices: &BTreeSet, +) -> State> { + State { + transitions: add_call_curry(s.transitions, other_init, accepting_indices), + non_accepting: s.non_accepting, + } +} + +/// Add a call to any accepting state. +#[inline] +#[must_use] +fn add_call_curry>( + s: Curry, + other_init: &BTreeSet, + accepting_indices: &BTreeSet, +) -> Curry> { + match s { + Curry::Wildcard(t) => { + Curry::Wildcard(add_call_transition(t, other_init, accepting_indices)) + } + Curry::Scrutinize { filter, fallback } => Curry::Scrutinize { + filter: add_call_range_map(filter, other_init, accepting_indices), + fallback: fallback.map(|f| add_call_transition(f, other_init, accepting_indices)), + }, + } +} + +/// Add a call to any accepting state. +#[inline] +#[must_use] +fn add_call_range_map>( + s: RangeMap, + other_init: &BTreeSet, + accepting_indices: &BTreeSet, +) -> RangeMap> { + RangeMap( + s.0.into_iter() + .map(|(k, v)| (k, add_call_transition(v, other_init, accepting_indices))) + .collect(), + ) +} + +/// Add a call to any accepting state. +#[inline] +#[must_use] +#[allow(clippy::todo)] // <-- FIXME +fn add_call_transition>( + s: Transition, + _other_init: &BTreeSet, + accepting_indices: &BTreeSet, +) -> Transition> { + match s { + Transition::Lateral { dst, update } => { + if dst.view().any(|ref i| accepting_indices.contains(i)) { + todo!() + } else { + Transition::Lateral { + dst: dst.view().collect(), + update, + } + } + } + Transition::Call { + region, + detour, + dst, + combine, + } => { + if dst.view().any(|ref i| accepting_indices.contains(i)) { + todo!() + } else { + Transition::Call { + region, + detour: detour.view().collect(), + dst: dst.view().collect(), + combine, + } + } + } + Transition::Return { region } => Transition::Return { region }, + } +}