diff --git a/examples/zero-copy.rs b/examples/zero-copy.rs index e92f7513..ce911055 100644 --- a/examples/zero-copy.rs +++ b/examples/zero-copy.rs @@ -1,4 +1,5 @@ use chumsky::prelude::*; +use chumsky::problems::Problems; #[derive(PartialEq, Debug)] enum Token<'a> { @@ -27,6 +28,17 @@ fn parser<'a>() -> impl Parser<'a, &'a str, [(SimpleSpan, Token<'a>); 6]> .collect_exactly() } +fn bad_parser<'a>() -> impl Parser<'a, &'a str, Vec<()>, extra::Err>> + Problems { + just("").or(just("b")) + .or_not() + .separated_by(just("")) + .ignored() + .then(just("").ignored()) + .ignored() + .repeated() + .collect::<_>() +} + fn main() { assert_eq!( parser() @@ -41,4 +53,9 @@ fn main() { ((31..37).into(), Token::Ident("tokens")), ]), ); + + let p = bad_parser(); + for p in p.find_problems() { + println!("{p}"); + } } diff --git a/src/lib.rs b/src/lib.rs index cb876ba0..aecc106f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,6 +61,7 @@ pub mod input; pub mod label; pub mod primitive; mod private; +pub mod problems; pub mod recovery; pub mod recursive; #[cfg(feature = "regex")] @@ -69,6 +70,7 @@ pub mod span; mod stream; pub mod text; pub mod util; +pub(crate) mod visit; /// Commonly used functions, traits and types. /// diff --git a/src/primitive.rs b/src/primitive.rs index bc3d87e8..645893c6 100644 --- a/src/primitive.rs +++ b/src/primitive.rs @@ -112,7 +112,7 @@ impl Default for JustCfg { /// See [`just`]. pub struct Just { - seq: T, + pub(crate) seq: T, #[allow(dead_code)] phantom: EmptyPhantom<(E, I)>, } @@ -838,7 +838,7 @@ where /// See [`choice`]. #[derive(Copy, Clone)] pub struct Choice { - parsers: T, + pub(crate) parsers: T, } /// Parse using a tuple of many parsers, producing the output of the first to successfully parse. diff --git a/src/problems.rs b/src/problems.rs new file mode 100644 index 00000000..d455c806 --- /dev/null +++ b/src/problems.rs @@ -0,0 +1,90 @@ +//! TODO + +use crate::visit::{ParserInfo, ParserVisitor, Visitable}; + +#[derive(Clone, PartialEq)] +enum CheckState { + None, + Check(String), + Defer(String), +} + +struct ProblemVisitor { + problems: Vec, + + first: bool, + check_nonempty: CheckState, +} + +impl ProblemVisitor { + fn new() -> Self { + ProblemVisitor { + problems: Vec::new(), + + first: true, + check_nonempty: CheckState::None, + } + } + + fn into_problems(self) -> Vec { + self.problems + } +} + +impl ParserVisitor for ProblemVisitor { + fn visit

(&mut self, info: &ParserInfo<'_, P>) + where + P: ?Sized + Visitable, + { + if self.first { + self.first = false; + if info.size_hint.lower() == 0 { + self.problems.push(format!( + "The top-level `{}` parser can potentially consume no input, meaning that it cannot fail.\nThis is probably a bug.\nConsider adding `.then_ignore(end())` to the parser.", info.name + )) + } + } + + let state = core::mem::replace(&mut self.check_nonempty, CheckState::None); + if let CheckState::Check(name) = state { + if info.size_hint.lower() == 0 { + self.problems.push(format!( + "`{}` parser has an inner parser that can potentially consume no input, meaning that it cannot fail.\nThis is probably a bug, because it could lead to an infinite loop.\nConsider using `.at_least(1)` to force the inner parser to consume some input.", + name, + )) + } + self.check_nonempty = CheckState::Defer(name); + } + + match info.name { + "repeated" | "separated_by" => { + let old = core::mem::replace( + &mut self.check_nonempty, + CheckState::Check(info.name.to_string()) + ); + info.visit(self); + self.check_nonempty = old; + } + _ => info.visit(self), + } + + let state = core::mem::replace(&mut self.check_nonempty, CheckState::None); + if let CheckState::Defer(name) = state { + self.check_nonempty = CheckState::Check(name); + } + } +} + +/// TODO +pub trait Problems: Visitable { + /// TODO + fn find_problems(&self) -> Vec; +} + +impl Problems for P { + fn find_problems(&self) -> Vec { + let mut visitor = ProblemVisitor::new(); + self.visit(&mut visitor); + visitor.into_problems() + } +} diff --git a/src/visit.rs b/src/visit.rs new file mode 100644 index 00000000..323809c0 --- /dev/null +++ b/src/visit.rs @@ -0,0 +1,287 @@ +use std::convert::TryFrom; +use super::*; +use crate::primitive::*; +use crate::combinator::*; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct SizeHint { + lower: usize, + upper: Option, +} + +impl SizeHint { + fn new(lower: usize, upper: Option) -> SizeHint { + SizeHint { + lower, + upper, + } + } + + fn and(lhs: SizeHint, rhs: SizeHint) -> SizeHint { + let lower = lhs.lower + rhs.lower; + let upper = lhs.upper.zip(rhs.upper).map(|(l, r)| l + r); + SizeHint::new(lower, upper) + } + + fn or(lhs: SizeHint, rhs: SizeHint) -> SizeHint { + let lower = usize::min(lhs.lower, rhs.lower); + let upper = lhs.upper.zip(rhs.upper).map(|(l, r)| usize::max(l, r)); + SizeHint::new(lower, upper) + } + + fn repeat(lhs: SizeHint, rhs: SizeHint) -> SizeHint { + let lower = lhs.lower * rhs.lower; + let upper = lhs.upper.zip(rhs.upper).map(|(this, reps)| { + reps * this + }); + SizeHint::new(lower, upper) + } + + pub fn lower(&self) -> usize { + self.lower + } + + pub fn upper(&self) -> Option { + self.upper + } +} + +pub struct ParserInfo<'a, P: ?Sized> { + pub(crate) name: &'a str, + pub(crate) size_hint: SizeHint, + pub(crate) parser: &'a P, +} + +impl<'a, P> ParserInfo<'a, P> +where + P: ?Sized + Visitable, +{ + fn new(parser: &'a P, name: &'a str, size_hint: SizeHint) -> ParserInfo<'a, P> { + ParserInfo { + name, + size_hint, + parser, + } + } + + pub fn visit(&self, visitor: &mut V) { + self.parser.visit_children(visitor); + } +} + +pub trait ParserVisitor { + fn visit

(&mut self, info: &ParserInfo<'_, P>) + where + P: ?Sized + Visitable; +} + +pub trait Visitable { + fn info(&self) -> ParserInfo<'_, Self>; + + fn visit(&self, visitor: &mut V) { + let info = self.info(); + visitor.visit(&info); + } + + fn visit_children(&self, visitor: &mut V) { + #![allow(unused_variables)] + } +} + +impl Visitable for Collect +where + A: Visitable, +{ + fn info(&self) -> ParserInfo<'_, Self> { + ParserInfo::new( + self, + "collect", + self.parser.info().size_hint + ) + } + + fn visit_children(&self, visitor: &mut V) { + self.parser.visit(visitor); + } +} + +impl Visitable for Ignored +where + A: Visitable +{ + fn info(&self) -> ParserInfo<'_, Self> { + ParserInfo::new( + self, + "ignored", + self.parser.info().size_hint, + ) + } + + fn visit_children(&self, visitor: &mut V) { + self.parser.visit(visitor); + } +} + +impl<'a, T, I: Input<'a>, E> Visitable for Just +where + T: OrderedSeq<'a, I::Token>, +{ + fn info(&self) -> ParserInfo<'_, Self> { + let size = self.seq.seq_iter() + .fold(0, |idx, _| idx + 1); + ParserInfo::new( + self, + "just", + SizeHint::new(size, Some(size)) + ) + } +} + +impl Visitable for Or +where + A: Visitable, + B: Visitable, +{ + fn info(&self) -> ParserInfo<'_, Self> { + ParserInfo::new( + self, + "or", + SizeHint::or( + self.choice.parsers.0.info().size_hint, + self.choice.parsers.1.info().size_hint, + ) + ) + } + + fn visit_children(&self, visitor: &mut V) { + self.choice.parsers.0.visit(visitor); + self.choice.parsers.1.visit(visitor); + } +} + +impl Visitable for OrNot +where + A: Visitable, +{ + fn info(&self) -> ParserInfo<'_, Self> { + ParserInfo::new( + self, + "or_not", + SizeHint::new(0, self.parser.info().size_hint.upper) + ) + } + + fn visit_children(&self, visitor: &mut V) { + self.parser.visit(visitor); + } +} + +impl Visitable for Repeated +where + A: Visitable, +{ + fn info(&self) -> ParserInfo<'_, Self> { + let at_most = if self.at_most == !0 { + None + } else { + Some(usize::try_from(self.at_most).unwrap_or(usize::MAX)) + }; + + ParserInfo::new( + self, + "repeated", + SizeHint::repeat( + self.parser.info().size_hint, + SizeHint::new(self.at_least, at_most) + ) + ) + } + + fn visit_children(&self, visitor: &mut V) { + self.parser.visit(visitor); + } +} + +impl Visitable for SeparatedBy +where + A: Visitable, + B: Visitable, +{ + fn info(&self) -> ParserInfo<'_, Self> { + let sep_size = self.separator.info().size_hint; + let item_size = self.parser.info().size_hint; + + let at_most = if self.at_most == !0 { + None + } else { + Some(usize::try_from(self.at_most).unwrap_or(usize::MAX)) + }; + + let min = if self.allow_leading && self.allow_trailing { + usize::min(sep_size.lower, item_size.lower) + } else { + item_size.lower + } * self.at_least; + + let lead_trail = self.allow_leading as usize + self.allow_trailing as usize; + let max = item_size.upper.zip(sep_size.upper) + .zip(at_most) + .map(|((item, sep), at_most)| { + ((item + sep) * at_most) + sep * lead_trail + }); + + ParserInfo::new( + self, + "separated_by", + SizeHint::new(min, max) + ) + } + + fn visit_children(&self, visitor: &mut V) { + self.parser.visit(visitor); + self.separator.visit(visitor); + } +} + +impl Visitable for CollectExactly +where + A: Visitable, + C: ContainerExactly, +{ + fn info(&self) -> ParserInfo<'_, Self> { + ParserInfo::new( + self, + "collect_exactly", + SizeHint::repeat( + self.parser.info().size_hint, + SizeHint::new(C::LEN, Some(C::LEN)) + ), + ) + } + + fn visit_children(&self, visitor: &mut V) { + self.parser.visit(visitor); + } +} + +impl Visitable for Then +where + A: Visitable, + B: Visitable, +{ + fn info(&self) -> ParserInfo<'_, Self> { + let a_info = self.parser_a.info(); + let b_info = self.parser_b.info(); + + ParserInfo::new( + self, + "then", + SizeHint::and(a_info.size_hint, b_info.size_hint) + ) + } + + fn visit_children(&self, visitor: &mut V) { + self.parser_a.visit(visitor); + self.parser_b.visit(visitor); + } +}