Skip to content

Commit

Permalink
Implement a Kleene-star operation (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
wrsturgeon authored Nov 13, 2023
2 parents 6eda682 + 2eced45 commit e2076f1
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 35 deletions.
12 changes: 1 addition & 11 deletions automata/src/ctrl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,9 @@ use std::collections::{btree_set, BTreeSet};
#[cfg(feature = "quickcheck")]
use core::num::NonZeroUsize;

/// Everything that could go wrong merging _any_ kind of indices.
/// No claim to be exhaustive: just the kinds that I've implemented so far.
#[non_exhaustive]
#[allow(clippy::module_name_repetitions)]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum CtrlMergeConflict {
/// Tried to merge two literal `usize`s that were not equal.
NotEqual(usize, usize),
}

/// Necessary preconditions to function as an index.
pub trait Ctrl<I: Input>:
Check<I, Self> + Clone + Merge<Error = CtrlMergeConflict> + Ord + PartialEq + ToSrc
Check<I, Self> + Clone + Merge<Error = (usize, usize)> + Ord + PartialEq + ToSrc
{
/// Non-owning view over each index in what may be a collection.
type View<'s>: Iterator<Item = usize>
Expand Down
41 changes: 40 additions & 1 deletion automata/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,6 @@ impl<I: Input, C: Ctrl<I>> Graph<I, C> {

/// Change nothing about the semantics but sort the internal vector of states.
#[inline]
#[allow(clippy::panic)] // <-- TODO
#[allow(clippy::missing_panics_doc)]
pub fn sort(&mut self) {
// Associate each original index with a concrete state instead of just an index,
Expand All @@ -298,6 +297,46 @@ impl<I: Input, C: Ctrl<I>> Graph<I, C> {
pub fn involves_any_fallback(&self) -> bool {
self.states.iter().any(State::involves_any_fallback)
}

/// Kleene-star operation: accept any number (including zero!) of repetitions of this parser.
#[inline]
#[must_use]
#[allow(clippy::panic, clippy::missing_panics_doc)]
pub fn star(self) -> Deterministic<I> {
let mut s = self.generalize();
let accepting: BTreeSet<usize> = s
.states
.iter()
.enumerate()
.filter(|&(_, st)| st.non_accepting.is_empty())
.map(|(i, _)| i)
.collect();
for state in &mut s.states {
match state.transitions {
Curry::Wildcard(ref mut t) => t.star(&s.initial, &accepting),
Curry::Scrutinize {
ref mut filter,
ref mut fallback,
} => {
filter.star(&s.initial, &accepting);
if let &mut Some(ref mut f) = fallback {
f.star(&s.initial, &accepting);
}
}
}
}
let empty = Graph {
states: vec![State {
transitions: Curry::Scrutinize {
filter: RangeMap(BTreeMap::new()),
fallback: None,
},
non_accepting: BTreeSet::new(),
}],
initial: 0,
};
empty | s.determinize().unwrap_or_else(|e| panic!("{e}"))
}
}

/// Use an ordering on subsets to translate each subset into a specific state.
Expand Down
2 changes: 1 addition & 1 deletion automata/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ mod qc;

pub use {
check::{Check, IllFormed},
ctrl::{Ctrl, CtrlMergeConflict},
ctrl::Ctrl,
curry::Curry,
f::{F, FF},
graph::{Deterministic, Graph, Nondeterministic},
Expand Down
28 changes: 13 additions & 15 deletions automata/src/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@

//! Trait to fallibly combine multiple values into one value with identical semantics.

use crate::{
Ctrl, CtrlMergeConflict, Curry, IllFormed, Input, RangeMap, State, Transition, Update, FF,
};
use crate::{Ctrl, Curry, IllFormed, Input, RangeMap, State, Transition, Update, FF};
use core::convert::Infallible;
use std::collections::{btree_map::Entry, BTreeMap, BTreeSet};

Expand Down Expand Up @@ -59,13 +57,13 @@ impl<T> Merge for Option<T> {
}

impl Merge for usize {
type Error = CtrlMergeConflict;
type Error = (usize, usize);
#[inline]
fn merge(self, other: Self) -> Result<Self, Self::Error> {
if self == other {
Ok(self)
} else {
Err(CtrlMergeConflict::NotEqual(self, other))
Err((self, other))
}
}
}
Expand All @@ -83,7 +81,7 @@ impl<'s> Merge for &'s str {
}

impl<T: Ord> Merge for BTreeSet<T> {
type Error = CtrlMergeConflict;
type Error = (usize, usize);
#[inline]
fn merge(mut self, other: Self) -> Result<Self, Self::Error> {
self.extend(other);
Expand Down Expand Up @@ -194,9 +192,9 @@ impl<I: Input, C: Ctrl<I>> Merge for Transition<I, C> {
update: r_update,
},
) => Ok(Self::Lateral {
dst: l_dst.merge(r_dst).map_err(|e| match e {
CtrlMergeConflict::NotEqual(a, b) => IllFormed::Superposition(a, b),
})?,
dst: l_dst
.merge(r_dst)
.map_err(|(a, b)| IllFormed::Superposition(a, b))?,
update: l_update
.merge(r_update)
.map_err(|(a, b)| IllFormed::IncompatibleCallbacks(Box::new(a), Box::new(b)))?,
Expand All @@ -218,12 +216,12 @@ impl<I: Input, C: Ctrl<I>> Merge for Transition<I, C> {
region: l_region
.merge(r_region)
.map_err(|(a, b)| IllFormed::AmbiguousRegions(a, b))?,
detour: l_detour.merge(r_detour).map_err(|e| match e {
CtrlMergeConflict::NotEqual(a, b) => IllFormed::Superposition(a, b),
})?,
dst: l_dst.merge(r_dst).map_err(|e| match e {
CtrlMergeConflict::NotEqual(a, b) => IllFormed::Superposition(a, b),
})?,
detour: l_detour
.merge(r_detour)
.map_err(|(a, b)| IllFormed::Superposition(a, b))?,
dst: l_dst
.merge(r_dst)
.map_err(|(a, b)| IllFormed::Superposition(a, b))?,
combine: l_combine.merge(r_combine).map_err(|(a, b)| {
IllFormed::IncompatibleCombinators(Box::new(a), Box::new(b))
})?,
Expand Down
12 changes: 11 additions & 1 deletion automata/src/range_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

use crate::{Ctrl, IllFormed, Input, Range, Transition};
use core::cmp;
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};

/// Map from ranges of keys to values.
#[repr(transparent)]
Expand Down Expand Up @@ -134,3 +134,13 @@ impl<I: Input> RangeMap<I, usize> {
)
}
}

impl<I: Input> RangeMap<I, BTreeSet<usize>> {
/// Kleene-star operation: accept any number (including zero!) of repetitions of this parser.
#[inline]
pub fn star(&mut self, init: &BTreeSet<usize>, accepting: &BTreeSet<usize>) {
for t in self.0.values_mut() {
t.star(init, accepting);
}
}
}
102 changes: 100 additions & 2 deletions automata/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,29 @@
clippy::use_debug
)]

use crate::*;

#[inline]
#[must_use]
fn splittable<I: Input, C: Ctrl<I>>(parser: &Graph<I, C>, input: &[I]) -> bool {
match input.len() {
0 => true,
len => {
for i in (1..=len).rev() {
if parser.accept(get!(input, ..i).iter().cloned()).is_ok()
&& splittable(parser, get!(input, i..))
{
return true;
}
}
false
}
}
}

#[cfg(feature = "quickcheck")]
mod prop {
use crate::*;
use super::*;
use core::num::NonZeroUsize;
use quickcheck::*;
use std::{collections::BTreeSet, env, panic};
Expand Down Expand Up @@ -304,11 +324,24 @@ mod prop {
}
concat.accept(input).is_ok() == splittable
}

fn star(d: Deterministic<u8>, input: Vec<u8>) -> bool {
if d.involves_any_fallback() {
return true;
}
if !splittable(&d, &input) {
return true;
}
let Ok(star) = panic::catch_unwind(|| d.star()) else {
return true;
};
star.accept(input).is_ok()
}
}
}

mod reduced {
use crate::*;
use super::*;
use std::{
collections::{BTreeMap, BTreeSet},
panic,
Expand Down Expand Up @@ -414,6 +447,20 @@ mod reduced {
assert_eq!(concat.accept(input).is_ok(), splittable);
}

fn star(d: Deterministic<u8>, input: Vec<u8>) {
if d.involves_any_fallback() {
return;
}
if !splittable(&d, &input) {
return;
}
let Ok(star) = panic::catch_unwind(|| d.star()) else {
return;
};
println!("{star:?}");
drop(star.accept(input).unwrap());
}

#[test]
fn deterministic_implies_no_runtime_errors_1() {
deterministic_implies_no_runtime_errors(
Expand Down Expand Up @@ -601,4 +648,55 @@ mod reduced {
vec![0],
);
}

#[test]
fn star_1() {
star(
Graph {
states: vec![
State {
transitions: Curry::Wildcard(Transition::Return { region: "region" }),
non_accepting: BTreeSet::new(),
},
State {
transitions: Curry::Wildcard(Transition::Call {
region: "region",
detour: 0,
dst: 0,
combine: ff!(|(), ()| ()),
}),
non_accepting: BTreeSet::new(),
},
],
initial: 1,
},
vec![],
);
}

#[test]
fn star_2() {
star(
Graph {
states: vec![
State {
transitions: Curry::Wildcard(Transition::Return { region: "region" }),
non_accepting: BTreeSet::new(),
},
State {
transitions: Curry::Scrutinize {
filter: RangeMap(BTreeMap::new()),
fallback: Some(Transition::Lateral {
dst: 0,
update: None,
}),
},
non_accepting: BTreeSet::new(),
},
],
initial: 1,
},
vec![0, 0],
);
}
}
5 changes: 3 additions & 2 deletions automata/src/to_src.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,12 +421,13 @@ impl<I: Input, C: Ctrl<I>> ToSrc for Curry<I, C> {
#[inline]
fn to_src(&self) -> String {
match *self {
Self::Wildcard(ref w) => format!("Curry::Wildcard({})", w.to_src()),
Self::Wildcard(ref w) => format!("{}::Wildcard({})", Self::src_type(), w.to_src()),
Self::Scrutinize {
ref filter,
ref fallback,
} => format!(
"Curry::Scrutinize{{ filter: {}, fallback: {} }}",
"{}::Scrutinize {{ filter: {}, fallback: {} }}",
Self::src_type(),
filter.to_src(),
fallback.to_src(),
),
Expand Down
21 changes: 19 additions & 2 deletions automata/src/transition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

//! Transition in an automaton: an action and a destination state.

use crate::{Ctrl, Input, InputError, ParseError, Update, FF};
use core::cmp;
use crate::{Ctrl, Input, InputError, Merge, ParseError, Update, FF};
use core::{cmp, mem};
use std::collections::BTreeSet;

// TODO: rename `Call` to `Open` and `Return` to `Close`

Expand Down Expand Up @@ -233,3 +234,19 @@ impl<I: Input> Transition<I, usize> {
}
}
}

impl<I: Input> Transition<I, BTreeSet<usize>> {
/// Kleene-star operation: accept any number (including zero!) of repetitions of this parser.
#[inline]
#[allow(clippy::missing_panics_doc)]
pub fn star(&mut self, init: &BTreeSet<usize>, accepting: &BTreeSet<usize>) {
match *self {
Transition::Lateral { ref mut dst, .. } | Transition::Call { ref mut dst, .. } => {
if dst.iter().any(|i| accepting.contains(i)) {
*dst = unwrap!(mem::take(dst).merge(init.clone()));
}
}
Transition::Return { .. } => {}
}
}
}

0 comments on commit e2076f1

Please sign in to comment.