Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Repeated deleting custom error and returning generic Unexpected error instead #483

Open
Cookie04DE opened this issue Jul 18, 2023 · 3 comments

Comments

@Cookie04DE
Copy link
Contributor

Consider the following code:

use std::fmt::{Debug, Display};

use chumsky::{prelude::*, util::Maybe};

#[derive(Debug, Clone, PartialEq, Eq)]
enum MyErr {
    Unexpected {
        expected: Vec<Option<char>>,
        found: Option<char>,
        span: SimpleSpan<usize>,
    },
    UnclosedComment {
        start: SimpleSpan<usize>,
    },
}

impl Display for MyErr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        <Self as Debug>::fmt(&self, f)
    }
}

impl std::error::Error for MyErr {}

impl<'a> chumsky::error::Error<'a, &'a str> for MyErr {
    fn expected_found<
        E: IntoIterator<Item = Option<chumsky::util::MaybeRef<'a, <&'a str as Input<'a>>::Token>>>,
    >(
        expected: E,
        found: Option<chumsky::util::MaybeRef<'a, <&'a str as Input<'a>>::Token>>,
        span: <&'a str as Input<'a>>::Span,
    ) -> Self {
        MyErr::Unexpected {
            expected: expected
                .into_iter()
                .map(|m| m.map(Maybe::into_inner))
                .collect(),
            found: found.map(Maybe::into_inner),
            span,
        }
    }

    fn merge(self, other: Self) -> Self {
        match (self, other) {
            (MyErr::Unexpected { .. }, a) => a,
            (a, MyErr::Unexpected { .. }) => a,
            (a, _) => a,
        }
    }
}

fn comment<'a>() -> impl Parser<'a, &'a str, (), extra::Err<MyErr>> {
    just("#")
        .to_span()
        .then(choice((just("#").to(Some(())), empty().to(None))))
        .try_map(|(start, end), _| match end {
            Some(()) => Ok(()),
            None => Err(MyErr::UnclosedComment { start }),
        })
}

#[test]
fn test_comment_ok() {
    comment().parse("##").into_result().unwrap()
}

#[test]
fn test_comment_ok_repeated() {
    let input = "##".repeat(3);
    comment().repeated().parse(&input).into_result().unwrap();
}

#[test]
fn test_comment_unclosed() {
    assert_eq!(
        comment().parse("#").into_result().unwrap_err(),
        vec![MyErr::UnclosedComment {
            start: (0..1).into()
        }]
    )
}

#[test]
fn test_comment_unclosed_repeated() {
    let input = "##".repeat(3) + "#";
    assert_eq!(
        comment()
            .repeated()
            .parse(&input)
            .into_result()
            .unwrap_err(),
        vec![MyErr::UnclosedComment {
            start: (6..7).into()
        }]
    );
}

All tests beside test_comment_unclosed_repeated run successfully.

This is the output of the failing test:

thread 'test_comment_unclosed_repeated' panicked at 'assertion failed: `(left == right)`
  left: `[Unexpected { expected: [], found: Some('#'), span: 6..7 }]`,
 right: `[UnclosedComment { start: 6..7 }]`', src/lib.rs:86:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

As you can see, instead of the expected custom UnclosedComment error, a generic Unexpected error is returned instead.
I traced the issue to this line where my UnclosedComment error get's deleted.

@Cookie04DE Cookie04DE changed the title Repeated deleting error and returning generic "expected got" error Repeated deleting custom error and returning generic Unexpected error Jul 18, 2023
@Cookie04DE Cookie04DE changed the title Repeated deleting custom error and returning generic Unexpected error Repeated deleting custom error and returning generic Unexpected error instead Jul 18, 2023
@Cookie04DE
Copy link
Contributor Author

Cookie04DE commented Jul 20, 2023

Found a workaround:

#[test]
fn test_comment_unclosed_repeated() {
    let input = "##".repeat(3) + "#";
    assert_eq!(
        comment()
            .repeated()
            .collect::<Vec<()>>()
            .parse(&input)
            .into_result()
            .unwrap_err(),
        vec![MyErr::UnclosedComment {
            start: (6..7).into()
        }]
    );
}

Notice the additonal .collect::<Vec<()>>() after the .repeated().
Even if the previous behavior is correct and intended, this difference in behavior between calling .collect and not calling .collect is still very surprising to me.

@zesterer
Copy link
Owner

That's quite intriguing. The only difference between the two should be that the output of comment actually gets generated, but TryMap always generates output anyway (since the closure returning Err is behaviourally meaningful when parsing, of course). Is that definitely the only change being made for the workaround to work?

@Cookie04DE
Copy link
Contributor Author

Cookie04DE commented Jul 26, 2023

Yes, it is the only change that makes test_comment_unclosed_repeated transition from fail to pass.
Before inserting .collect::<Vec<()>>() it fails, and afterwards it passes.
I am testing with 1.0.0-alpha.4 by the way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants