diff --git a/Cargo.lock b/Cargo.lock index 264ec5f..9b4abd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,14 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "digit" +version = "0.1.0" +dependencies = [ + "inator", + "quickcheck", +] + [[package]] name = "env_logger" version = "0.8.4" @@ -51,6 +59,7 @@ name = "inator-automata" version = "0.1.0" dependencies = [ "quickcheck", + "rand", ] [[package]] @@ -71,6 +80,12 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "quickcheck" version = "1.0.3" @@ -88,6 +103,18 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", "rand_core", ] diff --git a/Cargo.toml b/Cargo.toml index e465c43..5f7b3aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,4 @@ +workspace = { members = ["examples/digit"] } [package] name = "inator" authors = ["Will Sturgeon "] diff --git a/automata/src/combinators.rs b/automata/src/combinators.rs index 971d985..8739e83 100644 --- a/automata/src/combinators.rs +++ b/automata/src/combinators.rs @@ -6,7 +6,7 @@ //! Operations on nondeterministic finite automata returning nondeterministic finite automata. -#![allow(clippy::manual_assert, clippy::match_wild_err_arm, clippy::panic)] +#![allow(clippy::match_wild_err_arm, clippy::panic)] use crate::{ Ctrl, CurryInput, CurryStack, Deterministic, Graph, Input, Merge, RangeMap, Stack, State, diff --git a/automata/src/lib.rs b/automata/src/lib.rs index d067b16..b4500e3 100644 --- a/automata/src/lib.rs +++ b/automata/src/lib.rs @@ -64,6 +64,7 @@ clippy::implicit_return, clippy::inline_always, clippy::let_underscore_untyped, + clippy::manual_assert, clippy::min_ident_chars, clippy::missing_trait_methods, clippy::mod_module_files, diff --git a/examples/digit/Cargo.toml b/examples/digit/Cargo.toml new file mode 100644 index 0000000..6363ef9 --- /dev/null +++ b/examples/digit/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "digit" +version = "0.1.0" +edition = "2021" + + +[build-dependencies] +inator = { path = "../.." } + +[dev-dependencies] +quickcheck = "1.0.3" diff --git a/examples/digit/build.rs b/examples/digit/build.rs new file mode 100644 index 0000000..9076fe4 --- /dev/null +++ b/examples/digit/build.rs @@ -0,0 +1,7 @@ +use std::io; + +type I = u8; + +fn main() -> Result, inator::IllFormed> { + inator::digit().to_file("src/parser.rs") +} diff --git a/examples/digit/src/main.rs b/examples/digit/src/main.rs new file mode 100644 index 0000000..e846638 --- /dev/null +++ b/examples/digit/src/main.rs @@ -0,0 +1,6 @@ +mod parser; + +#[cfg(test)] +mod test; + +fn main() {} diff --git a/examples/digit/src/parser.rs b/examples/digit/src/parser.rs new file mode 100644 index 0000000..3bd8c16 --- /dev/null +++ b/examples/digit/src/parser.rs @@ -0,0 +1,84 @@ +//! Automatically generated with [inator](https://crates.io/crates/inator). + +#![allow(dead_code, unused_variables)] + +/// Descriptive parsing error. +#[allow(dead_code)] +#[derive(Clone, Debug, PartialEq)] +pub enum Error { + /// Token without any relevant rule. + Absurd { + /// Index of the token that caused this error. + index: usize, + /// Particular token that didn't correspond to a rule. + token: u8, + }, + /// Token that would have closed a delimiter, but the delimiter wasn't open. + Unopened { + /// Index of the token that caused this error. + index: usize, + /// Type of thing that wasn't opened (e.g. parentheses). + delimiter: (), + /// What actually was open (e.g. you tried to close parentheses, but a bracket was open). + instead: Option<()>, + }, + /// After parsing all input, a delimiter remains open (e.g. "(a, b, c"). + Unclosed { + /// Index at which the delimiter was opened (e.g., for parentheses, the index of the relevant '('). + opened: usize, + /// Type of thing that wasn't closed (e.g. parentheses). + delimiter: (), + }, + /// Ended on a user-defined non-accepting state. + UserDefined { + /// User-defined error message. + messages: &'static [&'static str], + }, +} + +type R = Result<(Option<(usize, (), Option>)>, ()), Error>; + +#[repr(transparent)] +struct F(fn(&mut I, Option<()>, ()) -> R); + +#[inline] +pub fn parse>(input: I) -> Result<(), Error> { + match state_1(&mut input.into_iter().enumerate(), None, Default::default())? { + (None, out) => Ok(out), + (Some((index, context, None)), out) => panic!("Some(({index:?}, {context:?}, None))"), + (Some((index, delimiter, Some(F(_)))), _) => Err(Error::Unopened { + index, + delimiter, + instead: None, + }), + } +} + +#[inline] +fn state_0>(input: &mut I, context: Option<()>, acc: ()) -> R { + match input.next() { + None => Ok((None, acc)), + Some((index, token)) => match (&context, &token) { + _ => Err(Error::Absurd { index, token }), + }, + } +} + +#[inline] +fn state_1>(input: &mut I, context: Option<()>, acc: ()) -> R { + match input.next() { + None => Err(Error::UserDefined { + messages: &[ + "Expected only a single token on [b\'0\'..=b\'9\'] but got another token after it", + ], + }), + Some((index, token)) => match (&context, &token) { + (&_, &(b'0'..=b'9')) => match state_0(input, context, (|(), i| i)(acc, token))? { + (None, _) => todo!(), + (done @ Some((_, _, None)), acc) => Ok((done, acc)), + (Some((idx, ctx, Some(F(f)))), out) => f(input, Some(ctx), out), + }, + _ => Err(Error::Absurd { index, token }), + }, + } +} diff --git a/examples/digit/src/test.rs b/examples/digit/src/test.rs new file mode 100644 index 0000000..87218ae --- /dev/null +++ b/examples/digit/src/test.rs @@ -0,0 +1,8 @@ +use core::iter; + +quickcheck::quickcheck! { + fn correct(i: u8) -> bool { + let d = b'0' + (i % 10); + crate::parser::parse(iter::once(d)) == Ok(d) + } +} diff --git a/src/f.rs b/src/f.rs index 92045f1..00d3934 100644 --- a/src/f.rs +++ b/src/f.rs @@ -6,24 +6,38 @@ //! Function representations. +#![allow(clippy::module_name_repetitions)] + use inator_automata::ToSrc; /// One-argument function. -pub struct F1 { +#[non_exhaustive] +#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct F { + /// Source-code representation of this function. pub src: String, + /// Argument type. pub arg_t: String, + /// Output type. pub output_t: String, } /// Two-argument function. -pub struct F2 { +#[non_exhaustive] +#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct FF { + /// Source-code representation of this function. pub src: String, + /// Type of the first argument. pub lhs_t: String, + /// Type of the second argument. pub rhs_t: String, + /// Output type. pub output_t: String, } -impl F1 { +impl F { + /// Internals of the `f!(...)` macro. #[inline] #[must_use] pub fn _from_macro(src: String, _: fn(Arg) -> Output) -> Self { @@ -35,7 +49,8 @@ impl F1 { } } -impl F2 { +impl FF { + /// Internals of the `ff!(...)` macro. #[inline] #[must_use] pub fn _from_macro( diff --git a/src/fixpoint.rs b/src/fixpoint.rs index 0f1b144..f726b6e 100644 --- a/src/fixpoint.rs +++ b/src/fixpoint.rs @@ -22,7 +22,7 @@ pub struct Fixpoint { impl ops::Shr> for Fixpoint { type Output = Deterministic; #[inline] - #[allow(clippy::arithmetic_side_effects, clippy::manual_assert, clippy::panic)] + #[allow(clippy::arithmetic_side_effects, clippy::panic)] fn shr(self, mut rhs: Deterministic) -> Self::Output { if let Some(lhs) = self.etc { rhs = lhs >> rhs; @@ -37,7 +37,7 @@ impl ops::Shr> for Fixpoint { impl ops::Shr> for Deterministic { type Output = Fixpoint; #[inline] - #[allow(clippy::manual_assert, clippy::panic)] + #[allow(clippy::panic)] fn shr(self, mut rhs: Fixpoint) -> Self::Output { assert!( rhs.etc.is_none(), diff --git a/src/lib.rs b/src/lib.rs index ecb94c6..cca5d35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,6 +76,7 @@ clippy::implicit_return, clippy::inline_always, clippy::let_underscore_untyped, + clippy::manual_assert, clippy::min_ident_chars, clippy::missing_trait_methods, clippy::mod_module_files, @@ -155,11 +156,30 @@ macro_rules! get_mut { } */ +/// Unreachable state, but checked if we're debugging. +#[cfg(any(debug_assertions, test))] +macro_rules! never { + () => { + unreachable!() + }; +} + +/// Unreachable state, but checked if we're debugging. +#[cfg(not(any(debug_assertions, test)))] +macro_rules! never { + () => {{ + #[allow(unsafe_code, unused_unsafe)] + unsafe { + core::hint::unreachable_unchecked() + } + }}; +} + /// One-argument function. #[macro_export] macro_rules! f { ($ex:expr) => { - $crate::F1::_from_macro(stringify!($ex).to_owned(), $ex) + $crate::F::_from_macro(stringify!($ex).to_owned(), $ex) }; } @@ -167,7 +187,7 @@ macro_rules! f { #[macro_export] macro_rules! ff { ($ex:expr) => { - $crate::F2::_from_macro(stringify!($ex).to_owned(), $ex) + $crate::FF::_from_macro(stringify!($ex).to_owned(), $ex) }; } @@ -182,10 +202,10 @@ mod recurse; mod test; pub use { - f::{F1, F2}, + f::{F, FF}, fixpoint::{fixpoint, Fixpoint}, inator_automata::*, - num::integer, + num::{digit, integer}, recurse::{recurse, Recurse}, }; @@ -278,16 +298,20 @@ pub fn toss_range(range: Range) -> Deterministic { } /// Run this parser, then apply this function to the result. +/// # Panics +/// FIXME #[inline] #[must_use] +#[allow(clippy::needless_pass_by_value, clippy::todo)] // <-- TODO +#[allow(clippy::panic)] pub fn process>( parser: Graph, - combinator: F1, + combinator: F, ) -> Graph { let Ok(parser_output_t) = parser.output_type() else { panic!("Inconsistent types in the parser argument to `process`.") }; - if parser_output_t != Some(combinator.arg_t) { + if parser_output_t.as_ref() != Some(&combinator.arg_t) { panic!( "Called `process` with a function that wants an input of type `{}`, \ but the parser {}.", @@ -300,11 +324,15 @@ pub fn process>( } /// Save the current value and put it aside, run this second parser from scratch, then combine the results. +/// # Panics +/// FIXME #[inline] #[must_use] +#[allow(clippy::needless_pass_by_value, clippy::todo)] // <-- TODO +#[allow(clippy::panic)] pub fn combine>( parser: Graph, - combinator: F2, + _combinator: FF, ) -> Graph { let Ok(maybe_parser_input_t) = parser.input_type() else { panic!("Inconsistent types in the parser argument to `combine`.") diff --git a/src/num.rs b/src/num.rs index 5726bee..a1640ee 100644 --- a/src/num.rs +++ b/src/num.rs @@ -11,6 +11,7 @@ use inator_automata::*; /// Any digit character (0, 1, 2, 3, 4, 5, 6, 7, 8, 9). #[inline] +#[must_use] pub fn digit() -> Deterministic { any_of( Range { @@ -23,13 +24,12 @@ pub fn digit() -> Deterministic { /// An unsigned integer consisting only of digits (e.g., no sign, no decimal point, no commas, etc.). #[inline] +#[must_use] +#[allow(clippy::arithmetic_side_effects)] pub fn integer() -> Deterministic { let shape = process(digit(), f!(|i: u8| usize::from(i))) >> fixpoint("integer") >> combine(digit(), ff!(|a: usize, b: usize| a * 10 + b)) >> recurse("integer"); - match shape.determinize() { - Ok(d) => d.sort(), - Err(_) => unreachable!(), - } + shape.determinize().unwrap_or_else(|_| never!()) } diff --git a/src/recurse.rs b/src/recurse.rs index af28d8d..1ab6bb3 100644 --- a/src/recurse.rs +++ b/src/recurse.rs @@ -31,7 +31,7 @@ fn will_accept( impl> ops::Shr for Graph { type Output = Deterministic; #[inline] - #[allow(clippy::manual_assert, clippy::panic)] + #[allow(clippy::panic)] fn shr(self, rhs: Recurse) -> Self::Output { let accepting_indices = self.states