diff --git a/Pidgin.Tests/CatchTests.cs b/Pidgin.Tests/CatchTests.cs new file mode 100644 index 0000000..537ec41 --- /dev/null +++ b/Pidgin.Tests/CatchTests.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; + +using Xunit; + +namespace Pidgin.Tests; + +public partial class CatchTests : ParserTestBase +{ + [Fact] + public void TestString() + { + DoTest((p, x) => p.Parse(x), x => x, x => x); + } + + [Fact] + public void TestList() + { + DoTest((p, x) => p.Parse(x), x => x, x => x.ToCharArray()); + } + + [Fact] + public void TestReadOnlyList() + { + DoTest((p, x) => p.ParseReadOnlyList(x), x => x, x => x.ToCharArray()); + } + + [Fact] + public void TestEnumerator() + { + DoTest((p, x) => p.Parse(x), x => x, x => x.AsEnumerable()); + } + + [Fact] + public void TestReader() + { + DoTest((p, x) => p.Parse(x), x => x, x => new StringReader(x)); + } + + [Fact] + public void TestStream() + { + DoTest((p, x) => p.Parse(x), Encoding.ASCII.GetBytes, x => new MemoryStream(Encoding.ASCII.GetBytes(x))); + } + + [Fact] + public void TestSpan() + { + DoTest((p, x) => p.Parse(x.Span), x => x, x => x.AsMemory()); + } + + private static void DoTest( + Func>, TInput, Result>> parseFunc, + Func> render, + Func toInput + ) + where TToken : IEquatable + { + { + var parser = + Parser.Sequence(render("foo")) + .Or(Parser.Sequence(render("1throw")) + .Then(Parser.Sequence(render("after")) + .RecoverWith(e => throw new InvalidOperationException()))) + .Or(Parser.Sequence(render("2throw")) + .Then(Parser.Sequence(render("after")) + .RecoverWith(e => throw new NotImplementedException()))) + .Catch((e, i) => Parser.Any.Repeat(i)) + .Catch((e) => Parser.Any.Many()); + AssertSuccess(parseFunc(parser, toInput("foobar")), render("foo")); + AssertSuccess(parseFunc(parser, toInput("1throwafter")), render("after")); + AssertSuccess(parseFunc(parser, toInput("1throwandrecover")), render("1throwa")); // it should have consumed the "1throwa" but then backtracked + AssertSuccess(parseFunc(parser, toInput("1throwaftsomemore")), render("1throwaft")); // it should have consumed the "1throwaft" but then backtracked + AssertSuccess(parseFunc(parser, toInput("2throwafter")), render("after")); + AssertSuccess(parseFunc(parser, toInput("2throwandrecover")), render("2throwandrecover")); // it should have consumed the "2throwa" but then backtracked + AssertSuccess(parseFunc(parser, toInput("2throwaftsomemore")), render("2throwaftsomemore")); // it should have consumed the "2throwaft" but then backtracked + AssertFailure( + parseFunc(parser, toInput("f")), + new ParseError( + Maybe.Nothing(), + true, + ImmutableArray.Create(new Expected(ImmutableArray.CreateRange(render("foo")))), + 1, + SourcePosDelta.OneCol, + null + ) + ); + AssertFailure( + parseFunc(parser, toInput("")), + new ParseError( + Maybe.Nothing(), + true, + ImmutableArray.Create(new Expected(ImmutableArray.CreateRange(render("foo"))), new Expected(ImmutableArray.CreateRange(render("1throw"))), new Expected(ImmutableArray.CreateRange(render("2throw")))), + 0, + SourcePosDelta.Zero, + null + ) + ); + AssertFailure( + parseFunc(parser, toInput("foul")), + new ParseError( + Maybe.Just(render("u").Single()), + false, + ImmutableArray.Create(new Expected(ImmutableArray.CreateRange(render("foo")))), + 2, + new SourcePosDelta(0, 2), + null + ) + ); + } + } +} diff --git a/Pidgin/Parser.Catch.cs b/Pidgin/Parser.Catch.cs new file mode 100644 index 0000000..0a9f8c4 --- /dev/null +++ b/Pidgin/Parser.Catch.cs @@ -0,0 +1,65 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Pidgin; + +public abstract partial class Parser +{ + /// + /// Creates a parser which runs the current parser, running if it throws . + /// + /// The exception to catch. + /// A function which returns a parser to apply when the current parser throws . + /// A parser twhich runs the current parser, running if it throws . + public Parser Catch(Func> errorHandler) + where TException : Exception + { + return Catch((TException e, int _) => errorHandler(e)); + } + + /// + /// Creates a parser which runs the current parser, calling with the number of inputs consumed + /// by the current parser until failure if it throws . + /// + /// The exception to catch. + /// A function which returns a parser to apply when the current parser throws . + /// A parser twhich runs the current parser, running if it throws . + public Parser Catch(Func> errorHandler) + where TException : Exception + { + return new CatchParser(this, errorHandler); + } +} + +internal sealed class CatchParser : Parser + where TException : Exception +{ + private readonly Parser _parser; + + private readonly Func> _errorHandler; + + public CatchParser(Parser parser, Func> errorHandler) + { + _errorHandler = errorHandler; + _parser = parser; + } + + public override bool TryParse(ref ParseState state, ref PooledList> expecteds, [MaybeNullWhen(false)] out T result) + { + var bookmark = state.Bookmark(); + try + { + var success = _parser.TryParse(ref state, ref expecteds, out result); + state.DiscardBookmark(bookmark); + + return success; + } + catch (TException e) + { + var count = state.Location - bookmark; + state.Rewind(bookmark); + + return _errorHandler(e, count).TryParse(ref state, ref expecteds, out result); + } + } +}