-
Notifications
You must be signed in to change notification settings - Fork 73
A parser to recover from exceptions #144
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<TToken, TInput>( | ||
Func<Parser<TToken, IEnumerable<TToken>>, TInput, Result<TToken, IEnumerable<TToken>>> parseFunc, | ||
Func<string, IEnumerable<TToken>> render, | ||
Func<string, TInput> toInput | ||
) | ||
where TToken : IEquatable<TToken> | ||
{ | ||
{ | ||
var parser = | ||
Parser<TToken>.Sequence(render("foo")) | ||
.Or(Parser<TToken>.Sequence(render("1throw")) | ||
.Then(Parser<TToken>.Sequence(render("after")) | ||
.RecoverWith(e => throw new InvalidOperationException()))) | ||
.Or(Parser<TToken>.Sequence(render("2throw")) | ||
.Then(Parser<TToken>.Sequence(render("after")) | ||
.RecoverWith(e => throw new NotImplementedException()))) | ||
.Catch<InvalidOperationException>((e, i) => Parser<TToken>.Any.Repeat(i)) | ||
.Catch<NotImplementedException>((e) => Parser<TToken>.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<TToken>( | ||
Maybe.Nothing<TToken>(), | ||
true, | ||
ImmutableArray.Create(new Expected<TToken>(ImmutableArray.CreateRange(render("foo")))), | ||
1, | ||
SourcePosDelta.OneCol, | ||
null | ||
) | ||
); | ||
AssertFailure( | ||
parseFunc(parser, toInput("")), | ||
new ParseError<TToken>( | ||
Maybe.Nothing<TToken>(), | ||
true, | ||
ImmutableArray.Create(new Expected<TToken>(ImmutableArray.CreateRange(render("foo"))), new Expected<TToken>(ImmutableArray.CreateRange(render("1throw"))), new Expected<TToken>(ImmutableArray.CreateRange(render("2throw")))), | ||
0, | ||
SourcePosDelta.Zero, | ||
null | ||
) | ||
); | ||
AssertFailure( | ||
parseFunc(parser, toInput("foul")), | ||
new ParseError<TToken>( | ||
Maybe.Just(render("u").Single()), | ||
false, | ||
ImmutableArray.Create(new Expected<TToken>(ImmutableArray.CreateRange(render("foo")))), | ||
2, | ||
new SourcePosDelta(0, 2), | ||
null | ||
) | ||
); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
using System; | ||
using System.Diagnostics.CodeAnalysis; | ||
|
||
namespace Pidgin; | ||
|
||
public abstract partial class Parser<TToken, T> | ||
{ | ||
/// <summary> | ||
/// Creates a parser which runs the current parser, running <paramref name="errorHandler"/> if it throws <typeparamref name="TException"/>. | ||
/// </summary> | ||
/// <typeparam name="TException">The exception to catch.</typeparam> | ||
/// <param name="errorHandler">A function which returns a parser to apply when the current parser throws <typeparamref name="TException"/>.</param> | ||
/// <returns>A parser twhich runs the current parser, running <paramref name="errorHandler"/> if it throws <typeparamref name="TException"/>.</returns> | ||
public Parser<TToken, T> Catch<TException>(Func<TException, Parser<TToken, T>> errorHandler) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd suggest modelling this as a parser which returns an exception, so Parser<TToken, TException> Catch<TException>()
where TException : Exception |
||
where TException : Exception | ||
{ | ||
return Catch((TException e, int _) => errorHandler(e)); | ||
} | ||
|
||
/// <summary> | ||
/// Creates a parser which runs the current parser, calling <paramref name="errorHandler"/> with the number of inputs consumed | ||
/// by the current parser until failure if it throws <typeparamref name="TException"/>. | ||
/// </summary> | ||
/// <typeparam name="TException">The exception to catch.</typeparam> | ||
/// <param name="errorHandler">A function which returns a parser to apply when the current parser throws <typeparamref name="TException"/>.</param> | ||
/// <returns>A parser twhich runs the current parser, running <paramref name="errorHandler"/> if it throws <typeparamref name="TException"/>.</returns> | ||
public Parser<TToken, T> Catch<TException>(Func<TException, int, Parser<TToken, T>> errorHandler) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think there's much need for this overload - seems overly specific (there are other components of the state which get rewound too) and you should be able to code it up yourself using |
||
where TException : Exception | ||
{ | ||
return new CatchParser<TToken, T, TException>(this, errorHandler); | ||
} | ||
} | ||
|
||
internal sealed class CatchParser<TToken, T, TException> : Parser<TToken, T> | ||
where TException : Exception | ||
{ | ||
private readonly Parser<TToken, T> _parser; | ||
|
||
private readonly Func<TException, int, Parser<TToken, T>> _errorHandler; | ||
|
||
public CatchParser(Parser<TToken, T> parser, Func<TException, int, Parser<TToken, T>> errorHandler) | ||
{ | ||
_errorHandler = errorHandler; | ||
_parser = parser; | ||
} | ||
|
||
public override bool TryParse(ref ParseState<TToken> state, ref PooledList<Expected<TToken>> 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure that we should rewind here. I imagine that clients might want to (eg) get the Parser CatchAndRewind<TException>()
=> Try(this.Catch<TException>(e => Fail())); |
||
|
||
return _errorHandler(e, count).TryParse(ref state, ref expecteds, out result); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test seems a little too complex in my opinion. I don't think we need to test this with all the different token types, nor do we need to run the parser on lots of different test inputs.
I'd suggest just adding a method in
StringParserTests.cs
and test the three main cases: