diff --git a/automata/src/check.rs b/automata/src/check.rs index ec2da99..de3950b 100644 --- a/automata/src/check.rs +++ b/automata/src/check.rs @@ -37,8 +37,6 @@ pub enum IllFormed> { /// Second output possibility. possibility_2: Box>, }, - /// In a `Curry`, a wildcard would have erased a fallback output. - WildcardFallback, /// Can't go to two different (deterministic) states at the same time. Superposition(usize, usize), /// Can't call two different functions on half-constructed outputs at the same time. @@ -80,7 +78,6 @@ impl IllFormed { possibility_1: Box::new(possibility_1.convert_ctrl()), possibility_2: Box::new(possibility_2.convert_ctrl()), }, - IllFormed::WildcardFallback => IllFormed::WildcardFallback, IllFormed::Superposition(a, b) => IllFormed::Superposition(a, b), IllFormed::IncompatibleCallbacks(a, b) => IllFormed::IncompatibleCallbacks(a, b), IllFormed::IncompatibleCombinators(a, b) => IllFormed::IncompatibleCombinators(a, b), @@ -140,12 +137,6 @@ impl> fmt::Display for IllFormed { possibility_2.to_src(), ) } - Self::WildcardFallback => { - write!( - f, - "A wildcard match would have overwritten a fallback output", - ) - } Self::Superposition(a, b) => write!( f, "Tried to visit two different deterministic states \ diff --git a/automata/src/curry.rs b/automata/src/curry.rs index 88d66c6..a6744c0 100644 --- a/automata/src/curry.rs +++ b/automata/src/curry.rs @@ -221,6 +221,19 @@ impl> Curry { } => Box::new(filter.values_mut().chain(fallback)), } } + + /// Check if this parser ever could, at any point, involve a fallback transition. + #[inline] + #[must_use] + pub const fn involves_any_fallback(&self) -> bool { + matches!( + *self, + Self::Scrutinize { + fallback: Some(_), + .. + } + ) + } } impl Curry { diff --git a/automata/src/graph.rs b/automata/src/graph.rs index 653dbf9..4efee27 100644 --- a/automata/src/graph.rs +++ b/automata/src/graph.rs @@ -291,6 +291,13 @@ impl> Graph { .map(|s| s.reindex(&self.states, &index_map)) .collect(); } + + /// Check if this parser ever could, at any point, involve a fallback transition. + #[inline] + #[must_use] + pub fn involves_any_fallback(&self) -> bool { + self.states.iter().any(State::involves_any_fallback) + } } /// Use an ordering on subsets to translate each subset into a specific state. diff --git a/automata/src/merge.rs b/automata/src/merge.rs index c25fb15..2510b55 100644 --- a/automata/src/merge.rs +++ b/automata/src/merge.rs @@ -141,11 +141,10 @@ impl> Merge for Curry { fn merge(self, other: Self) -> Result { match (self, other) { (Self::Wildcard(lhs), Self::Wildcard(rhs)) => Ok(Self::Wildcard(lhs.merge(rhs)?)), - (Self::Wildcard(w), Self::Scrutinize { filter, fallback }) - | (Self::Scrutinize { filter, fallback }, Self::Wildcard(w)) => { + (Self::Wildcard(w), Self::Scrutinize { filter, .. }) + | (Self::Scrutinize { filter, .. }, Self::Wildcard(w)) => { match filter.0.first_key_value() { - None if fallback.is_none() => Ok(Self::Wildcard(w)), - None => Err(IllFormed::WildcardFallback), + None => Ok(Self::Wildcard(w)), Some((k, v)) => Err(IllFormed::WildcardMask { arg_token: Some(k.clone()), possibility_1: Box::new(w), diff --git a/automata/src/state.rs b/automata/src/state.rs index 1b3daed..98beac1 100644 --- a/automata/src/state.rs +++ b/automata/src/state.rs @@ -37,6 +37,13 @@ impl> State { }) }) } + + /// Check if this parser ever could, at any point, involve a fallback transition. + #[inline] + #[must_use] + pub const fn involves_any_fallback(&self) -> bool { + self.transitions.involves_any_fallback() + } } impl State { diff --git a/automata/src/test.rs b/automata/src/test.rs index 45f498e..a9ab249 100644 --- a/automata/src/test.rs +++ b/automata/src/test.rs @@ -15,24 +15,6 @@ clippy::use_debug )] -mod unit { - use crate::*; - use std::collections::BTreeMap; - - #[test] - fn check_reject_wildcard_mask_fallback() { - let lhs = Curry::<(), _>::Scrutinize { - filter: RangeMap(BTreeMap::new()), - fallback: Some(Transition::Lateral { - dst: 0, - update: None, - }), - }; - let rhs = Curry::<(), _>::Wildcard(Transition::Return { region: "region" }); - drop(lhs.merge(rhs).unwrap_err()); - } -} - #[cfg(feature = "quickcheck")] mod prop { use crate::*; @@ -239,6 +221,9 @@ mod prop { rhs: Deterministic, input: Vec ) -> bool { + if lhs.involves_any_fallback() || rhs.involves_any_fallback() { + return true; + } let Ok(union) = panic::catch_unwind(|| lhs.clone() | rhs.clone()) else { return true; }; @@ -249,7 +234,7 @@ mod prop { return true; } let union_accept = union.accept(input.iter().copied()); - match ( + if !match ( lhs.accept(input.iter().copied()), rhs.accept(input.iter().copied()), ) { @@ -271,7 +256,19 @@ mod prop { (Err(ParseError::BadInput(..)), Err(ParseError::BadInput(..))) => { union_accept.is_err() } + } { + return false; } + let Ok(symm) = panic::catch_unwind(|| rhs | lhs) else { + return false; + }; + if symm.check().is_err() { + return false; + } + if symm.determinize().is_err() { + return false; + } + union_accept == symm.accept(input.iter().copied()) } fn sort(parser: Nondeterministic, input: Vec) -> bool { @@ -289,6 +286,9 @@ mod prop { } fn shr(lhs: Deterministic, rhs: Deterministic, input: Vec) -> bool { + if lhs.involves_any_fallback() || rhs.involves_any_fallback() { + return true; + } let splittable = (0..=input.len()).any(|i| { lhs.accept(input[..i].iter().copied()).is_ok() && rhs.accept(input[i..].iter().copied()).is_ok() @@ -328,7 +328,10 @@ mod reduced { assert_eq!(dd.accept(input.iter().copied()), d.accept(input)); } - fn union(lhs: &Deterministic, rhs: &Deterministic, input: &[u8]) { + fn union(lhs: Deterministic, rhs: Deterministic, input: Vec) { + if lhs.involves_any_fallback() || rhs.involves_any_fallback() { + return; + } let Ok(union) = panic::catch_unwind(|| lhs.clone() | rhs.clone()) else { return; }; @@ -339,7 +342,7 @@ mod reduced { { println!(); println!("LHS:"); - let mut run = input.iter().copied().run(lhs); + let mut run = input.iter().copied().run(&lhs); println!(" {run:?}"); while let Some(r) = run.next() { println!("{r:?} {run:?}"); @@ -348,7 +351,7 @@ mod reduced { { println!(); println!("RHS:"); - let mut run = input.iter().copied().run(rhs); + let mut run = input.iter().copied().run(&rhs); println!(" {run:?}"); while let Some(r) = run.next() { println!("{r:?} {run:?}"); @@ -384,12 +387,19 @@ mod reduced { assert!(matches!(union_accept, Err(ParseError::BadParser(..)))); } (Err(ParseError::BadInput(..)), Err(ParseError::BadInput(..))) => { - drop(union_accept.unwrap_err()); + let _ = union_accept.as_ref().unwrap_err(); } } + let symm = rhs | lhs; + symm.check().unwrap(); + drop(symm.determinize().unwrap()); + assert_eq!(union_accept, symm.accept(input)); } fn shr(lhs: Deterministic, rhs: Deterministic, input: Vec) { + if lhs.involves_any_fallback() || rhs.involves_any_fallback() { + return; + } let splittable = (0..=input.len()).any(|i| { lhs.accept(input[..i].iter().copied()).is_ok() && rhs.accept(input[i..].iter().copied()).is_ok() @@ -469,7 +479,7 @@ mod reduced { #[test] fn union_1() { union( - &Graph { + Graph { states: vec![State { transitions: Curry::Scrutinize { filter: RangeMap(BTreeMap::new()), @@ -482,14 +492,50 @@ mod reduced { }], initial: 0, }, - &Graph { + Graph { states: vec![State { transitions: Curry::Wildcard(Transition::Return { region: "region" }), non_accepting: BTreeSet::new(), }], initial: 0, }, - &[0], + vec![0], + ); + } + + #[test] + fn union_2() { + union( + Graph { + states: vec![State { + transitions: Curry::Scrutinize { + filter: RangeMap( + iter::once(( + Range { first: 0, last: 0 }, + Transition::Return { region: "region" }, + )) + .collect(), + ), + fallback: None, + }, + non_accepting: BTreeSet::new(), + }], + initial: 0, + }, + Graph { + states: vec![State { + transitions: Curry::Scrutinize { + filter: RangeMap(BTreeMap::new()), + fallback: Some(Transition::Lateral { + dst: 0, + update: None, + }), + }, + non_accepting: BTreeSet::new(), + }], + initial: 0, + }, + vec![0], ); } @@ -519,4 +565,40 @@ mod reduced { vec![0], ); } + + #[test] + fn shr_2() { + shr( + Graph { + states: vec![State { + transitions: Curry::Scrutinize { + filter: RangeMap( + iter::once(( + Range { first: 0, last: 0 }, + Transition::Return { region: "region" }, + )) + .collect(), + ), + fallback: None, + }, + non_accepting: BTreeSet::new(), + }], + initial: 0, + }, + Graph { + states: vec![State { + transitions: Curry::Scrutinize { + filter: RangeMap(BTreeMap::new()), + fallback: Some(Transition::Lateral { + dst: 0, + update: None, + }), + }, + non_accepting: BTreeSet::new(), + }], + initial: 0, + }, + vec![0], + ); + } }