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

A parser to recover from exceptions #144

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Pidgin.Tests/CatchTestException1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;

namespace Pidgin.Tests;

public class CatchTest1Exception : Exception
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can make this a private class rather than a separate one, or (my preference) just use some off-the-shelf exception types (eg InvalidOperationException and the like)

{
public CatchTest1Exception()
{
}

public CatchTest1Exception(string message)
: base(message)
{
}

public CatchTest1Exception(string message, Exception innerException)
: base(message, innerException)
{
}
}
20 changes: 20 additions & 0 deletions Pidgin.Tests/CatchTestException2.cs
Original file line number Diff line number Diff line change
@@ -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)
{
}
}
116 changes: 116 additions & 0 deletions Pidgin.Tests/CatchTests.cs
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
Copy link
Owner

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:

  1. Throwing the expected exception
  2. Throwing a different exception
  3. Throwing no exception

{
[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 CatchTest1Exception())))
.Or(Parser<TToken>.Sequence(render("2throw"))
.Then(Parser<TToken>.Sequence(render("after"))
.RecoverWith(e => throw new CatchTest2Exception())))
.Catch<CatchTest1Exception>((e, i) => Parser<TToken>.Any.Repeat(i))
.Catch<CatchTest2Exception>((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
)
);
}
}
}
76 changes: 76 additions & 0 deletions Pidgin/Parser.Catch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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)
Copy link
Owner

Choose a reason for hiding this comment

The 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)
Copy link
Owner

Choose a reason for hiding this comment

The 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 CurrentOffset.

where TException : Exception
{
return new CatchParser<TToken, T, TException>(this, errorHandler);
}
}

/// <summary>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for xmldocs on internal classes

/// A parser to wrap previous parser and catch exceptions of specified type and provides
/// an opportunity to recover.
/// </summary>
/// <remarks>
/// The previous parser may e.g. throw an exception in a delegate used with
/// <see cref="Parser{TToken, T}.Select{U}(Func{T, U})"/>.
/// </remarks>
/// <typeparam name="TToken">The type of tokens in the parser's input stream.</typeparam>
/// <typeparam name="T">The return type of the parser.</typeparam>
/// <typeparam name="TException">Exception type to catch.</typeparam>
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);
Copy link
Owner

Choose a reason for hiding this comment

The 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 CurrentPos at the point of failure. Should be feasible to code up the rewind logic yourself, ie

Parser CatchAndRewind<TException>()
    => Try(this.Catch<TException>(e => Fail()));


return _errorHandler(e, count).TryParse(ref state, ref expecteds, out result);
}
}
}