From cd140cab43af0de6a12d60831577d8389dda0d11 Mon Sep 17 00:00:00 2001 From: eriksunsol Date: Mon, 5 Jun 2023 15:37:03 +0200 Subject: [PATCH 1/2] Add class, methods and test for CatchParser --- Pidgin.Tests/CatchTestException1.cs | 20 +++++ Pidgin.Tests/CatchTestException2.cs | 20 +++++ Pidgin.Tests/CatchTests.cs | 116 ++++++++++++++++++++++++++++ Pidgin/Parser.Catch.cs | 76 ++++++++++++++++++ 4 files changed, 232 insertions(+) create mode 100644 Pidgin.Tests/CatchTestException1.cs create mode 100644 Pidgin.Tests/CatchTestException2.cs create mode 100644 Pidgin.Tests/CatchTests.cs create mode 100644 Pidgin/Parser.Catch.cs diff --git a/Pidgin.Tests/CatchTestException1.cs b/Pidgin.Tests/CatchTestException1.cs new file mode 100644 index 0000000..ad84911 --- /dev/null +++ b/Pidgin.Tests/CatchTestException1.cs @@ -0,0 +1,20 @@ +using System; + +namespace Pidgin.Tests; + +public class CatchTest1Exception : Exception +{ + public CatchTest1Exception() + { + } + + public CatchTest1Exception(string message) + : base(message) + { + } + + public CatchTest1Exception(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/Pidgin.Tests/CatchTestException2.cs b/Pidgin.Tests/CatchTestException2.cs new file mode 100644 index 0000000..6234043 --- /dev/null +++ b/Pidgin.Tests/CatchTestException2.cs @@ -0,0 +1,20 @@ +using System; + +namespace Pidgin.Tests; + +public class CatchTest2Exception : Exception +{ + public CatchTest2Exception() + { + } + + public CatchTest2Exception(string message) + : base(message) + { + } + + public CatchTest2Exception(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/Pidgin.Tests/CatchTests.cs b/Pidgin.Tests/CatchTests.cs new file mode 100644 index 0000000..f1b1ed5 --- /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 CatchTest1Exception()))) + .Or(Parser.Sequence(render("2throw")) + .Then(Parser.Sequence(render("after")) + .RecoverWith(e => throw new CatchTest2Exception()))) + .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..18cdd0d --- /dev/null +++ b/Pidgin/Parser.Catch.cs @@ -0,0 +1,76 @@ +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); + } +} + +/// +/// A parser to wrap previous parser and catch exceptions of specified type and provides +/// an opportunity to recover. +/// +/// +/// The previous parser may e.g. throw an exception in a delegate used with +/// . +/// +/// The type of tokens in the parser's input stream. +/// The return type of the parser. +/// Exception type to catch. +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); + } + } +} From 2074efbc8405ffedc9abcd5748cbc6d8fe9b03e7 Mon Sep 17 00:00:00 2001 From: eriksunsol Date: Thu, 22 Jun 2023 13:13:38 +0200 Subject: [PATCH 2/2] Fixed some trivial review comments. --- Pidgin.Tests/CatchTestException1.cs | 20 -------------------- Pidgin.Tests/CatchTestException2.cs | 20 -------------------- Pidgin.Tests/CatchTests.cs | 8 ++++---- Pidgin/Parser.Catch.cs | 11 ----------- 4 files changed, 4 insertions(+), 55 deletions(-) delete mode 100644 Pidgin.Tests/CatchTestException1.cs delete mode 100644 Pidgin.Tests/CatchTestException2.cs diff --git a/Pidgin.Tests/CatchTestException1.cs b/Pidgin.Tests/CatchTestException1.cs deleted file mode 100644 index ad84911..0000000 --- a/Pidgin.Tests/CatchTestException1.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Pidgin.Tests; - -public class CatchTest1Exception : Exception -{ - public CatchTest1Exception() - { - } - - public CatchTest1Exception(string message) - : base(message) - { - } - - public CatchTest1Exception(string message, Exception innerException) - : base(message, innerException) - { - } -} diff --git a/Pidgin.Tests/CatchTestException2.cs b/Pidgin.Tests/CatchTestException2.cs deleted file mode 100644 index 6234043..0000000 --- a/Pidgin.Tests/CatchTestException2.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Pidgin.Tests; - -public class CatchTest2Exception : Exception -{ - public CatchTest2Exception() - { - } - - public CatchTest2Exception(string message) - : base(message) - { - } - - public CatchTest2Exception(string message, Exception innerException) - : base(message, innerException) - { - } -} diff --git a/Pidgin.Tests/CatchTests.cs b/Pidgin.Tests/CatchTests.cs index f1b1ed5..537ec41 100644 --- a/Pidgin.Tests/CatchTests.cs +++ b/Pidgin.Tests/CatchTests.cs @@ -65,12 +65,12 @@ Func toInput Parser.Sequence(render("foo")) .Or(Parser.Sequence(render("1throw")) .Then(Parser.Sequence(render("after")) - .RecoverWith(e => throw new CatchTest1Exception()))) + .RecoverWith(e => throw new InvalidOperationException()))) .Or(Parser.Sequence(render("2throw")) .Then(Parser.Sequence(render("after")) - .RecoverWith(e => throw new CatchTest2Exception()))) - .Catch((e, i) => Parser.Any.Repeat(i)) - .Catch((e) => Parser.Any.Many()); + .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 diff --git a/Pidgin/Parser.Catch.cs b/Pidgin/Parser.Catch.cs index 18cdd0d..0a9f8c4 100644 --- a/Pidgin/Parser.Catch.cs +++ b/Pidgin/Parser.Catch.cs @@ -31,17 +31,6 @@ public Parser Catch(Func -/// A parser to wrap previous parser and catch exceptions of specified type and provides -/// an opportunity to recover. -/// -/// -/// The previous parser may e.g. throw an exception in a delegate used with -/// . -/// -/// The type of tokens in the parser's input stream. -/// The return type of the parser. -/// Exception type to catch. internal sealed class CatchParser : Parser where TException : Exception {