Skip to content

Commit

Permalink
Add If parser and improve When (#186)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastienros authored Dec 14, 2024
1 parent 4951f01 commit e370f53
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 11 deletions.
18 changes: 15 additions & 3 deletions docs/parsers.md
Original file line number Diff line number Diff line change
Expand Up @@ -780,12 +780,24 @@ failure: "Unexpected char c"

### When

Adds some additional logic for a parser to succeed.
Adds some additional logic for a parser to succeed. The condition is executed when the previous parser succeeds. If the predicate returns `false`, the parser fails.

```c#
Parser<T> When(Func<T, bool> predicate)
Parser<T> When(Func<ParseContext, T, bool> predicate)
```

To evaluate a condition before a parser is executed use the `If` parser instead.

### If

Executes a parser only if a condition is true.

```c#
Parser<T> If<TContext, TState, T>(Func<ParseContext, TState, bool> predicate, TState state, Parser<T> parser)
```

To evaluate a condition before a parser is executed use the `If` parser instead.

### Switch

Returns the next parser based on some custom logic that can't be defined statically. It is typically used in conjunction with a `ParseContext` instance
Expand All @@ -805,7 +817,7 @@ var parser = Terms.Integer().And(Switch((context, x) =>
});
```

For performance reasons it is recommended to return a static Parser instance. Otherwise each `Parse` execution will allocate and it will usually be the same objects.
For performance reasons it is recommended to return a singleton (or static) Parser instance. Otherwise each `Parse` execution will allocate a new Parser instance.

### Eof

Expand Down
110 changes: 110 additions & 0 deletions src/Parlot/Fluent/If.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using Parlot.Compilation;
using System;
#if NET
using System.Linq;
#endif
using System.Linq.Expressions;

namespace Parlot.Fluent;

/// <summary>
/// Ensure the given parser is valid based on a condition, and backtracks if not.
/// </summary>
/// <typeparam name="C">The concrete <see cref="ParseContext" /> type to use.</typeparam>
/// <typeparam name="S">The type of the state to pass.</typeparam>
/// <typeparam name="T">The output parser type.</typeparam>
public sealed class If<C, S, T> : Parser<T>, ICompilable where C : ParseContext
{
private readonly Func<C, S?, bool> _predicate;
private readonly S? _state;
private readonly Parser<T> _parser;

public If(Parser<T> parser, Func<C, S?, bool> predicate, S? state)
{
_predicate = predicate ?? throw new ArgumentNullException(nameof(predicate));
_state = state;
_parser = parser ?? throw new ArgumentNullException(nameof(parser));

Name = $"{parser.Name} (If)";
}

public override bool Parse(ParseContext context, ref ParseResult<T> result)
{
context.EnterParser(this);

var valid = _predicate((C)context, _state);

if (valid)
{
var start = context.Scanner.Cursor.Position;

if (!_parser.Parse(context, ref result))
{
context.Scanner.Cursor.ResetPosition(start);
}
}

context.ExitParser(this);
return valid;
}

public CompilationResult Compile(CompilationContext context)
{
var result = context.CreateCompilationResult<T>();

var parserCompileResult = _parser.Build(context, requireResult: true);

// success = false;
// value = default;
//
// start = context.Scanner.Cursor.Position;
// if (_predicate((C)context, _state) )
// {
// parser instructions
//
// if (parser.success)
// {
// success = true;
// value = parser.Value;
// }
// }
//
// if (!success)
// {
// context.ResetPosition(start);
// }
//

var start = context.DeclarePositionVariable(result);
// parserCompileResult.Success

var block = Expression.Block(
Expression.IfThen(
Expression.Invoke(Expression.Constant(_predicate), [Expression.Convert(context.ParseContext, typeof(C)), Expression.Constant(_state, typeof(S))]),
Expression.Block(
Expression.Block(
parserCompileResult.Variables,
parserCompileResult.Body),
Expression.IfThen(
parserCompileResult.Success,
Expression.Block(
Expression.Assign(result.Success, Expression.Constant(true, typeof(bool))),
context.DiscardResult
? Expression.Empty()
: Expression.Assign(result.Value, parserCompileResult.Value)
)
)
)
),
Expression.IfThen(
Expression.Not(result.Success),
context.ResetPosition(start)
)
);


result.Body.Add(block);

return result;
}
}
6 changes: 6 additions & 0 deletions src/Parlot/Fluent/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,14 @@ public Parser<T> Named(string name)
/// <summary>
/// Builds a parser that verifies the previous parser result matches a predicate.
/// </summary>
[Obsolete("Use When(Func<ParseContext, T, bool> predicate) instead.")]
public Parser<T> When(Func<T, bool> predicate) => new When<T>(this, predicate);

/// <summary>
/// Builds a parser that verifies the previous parser result matches a predicate.
/// </summary>
public Parser<T> When(Func<ParseContext, T, bool> predicate) => new When<T>(this, predicate);

/// <summary>
/// Builds a parser what returns another one based on the previous result.
/// </summary>
Expand Down
20 changes: 20 additions & 0 deletions src/Parlot/Fluent/Parsers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,26 @@ public static partial class Parsers
/// </summary>
public static Parser<T> Not<T>(Parser<T> parser) => new Not<T>(parser);

/// <summary>
/// Builds a parser that invoked the next one if a condition is true.
/// </summary>
public static Parser<T> If<C, S, T>(Func<C, S?, bool> predicate, S? state, Parser<T> parser) where C : ParseContext => new If<C, S, T>(parser, predicate, state);

/// <summary>
/// Builds a parser that invoked the next one if a condition is true.
/// </summary>
public static Parser<T> If<S, T>(Func<ParseContext, S?, bool> predicate, S? state, Parser<T> parser) => new If<ParseContext, S, T>(parser, predicate, state);

/// <summary>
/// Builds a parser that invoked the next one if a condition is true.
/// </summary>
public static Parser<T> If<C, T>(Func<C, bool> predicate, Parser<T> parser) where C : ParseContext => new If<C, object?, T>(parser, (c, s) => predicate(c), null);

/// <summary>
/// Builds a parser that invoked the next one if a condition is true.
/// </summary>
public static Parser<T> If<T>(Func<ParseContext, bool> predicate, Parser<T> parser) => new If<ParseContext, object?, T>(parser, (c, s) => predicate(c), null);

/// <summary>
/// Builds a parser that can be defined later one. Use it when a parser need to be declared before its rule can be set.
/// </summary>
Expand Down
17 changes: 14 additions & 3 deletions src/Parlot/Fluent/When.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Parlot.Compilation;
using System;
#if NET
using System.Linq;
#endif
using System.Linq.Expressions;

namespace Parlot.Fluent;
Expand All @@ -11,10 +13,19 @@ namespace Parlot.Fluent;
/// <typeparam name="T">The output parser type.</typeparam>
public sealed class When<T> : Parser<T>, ICompilable
{
private readonly Func<T, bool> _action;
private readonly Func<ParseContext, T, bool> _action;
private readonly Parser<T> _parser;

[Obsolete("Use When(Parser<T> parser, Func<ParseContext, T, bool> action) instead.")]
public When(Parser<T> parser, Func<T, bool> action)
{
_action = action != null ? (c, t) => action(t) : throw new ArgumentNullException(nameof(action));
_parser = parser ?? throw new ArgumentNullException(nameof(parser));

Name = $"{parser.Name} (When)";
}

public When(Parser<T> parser, Func<ParseContext, T, bool> action)
{
_action = action ?? throw new ArgumentNullException(nameof(action));
_parser = parser ?? throw new ArgumentNullException(nameof(parser));
Expand All @@ -28,7 +39,7 @@ public override bool Parse(ParseContext context, ref ParseResult<T> result)

var start = context.Scanner.Cursor.Position;

var valid = _parser.Parse(context, ref result) && _action(result.Value);
var valid = _parser.Parse(context, ref result) && _action(context, result.Value);

if (!valid)
{
Expand Down Expand Up @@ -70,7 +81,7 @@ public CompilationResult Compile(CompilationContext context)
Expression.IfThenElse(
Expression.AndAlso(
parserCompileResult.Success,
Expression.Invoke(Expression.Constant(_action), new[] { parserCompileResult.Value })
Expression.Invoke(Expression.Constant(_action), [context.ParseContext, parserCompileResult.Value])
),
Expression.Block(
Expression.Assign(result.Success, Expression.Constant(true, typeof(bool))),
Expand Down
19 changes: 17 additions & 2 deletions test/Parlot.Tests/CompileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ public void CompiledIdentifierShouldAcceptExtraChars(string text)
[Fact]
public void CompiledWhenShouldFailParserWhenFalse()
{
var evenIntegers = Literals.Integer().When(x => x % 2 == 0).Compile();
var evenIntegers = Literals.Integer().When((c, x) => x % 2 == 0).Compile();

Assert.True(evenIntegers.TryParse("1234", out var result1));
Assert.Equal(1234, result1);
Expand All @@ -452,12 +452,27 @@ public void CompiledWhenShouldFailParserWhenFalse()
[Fact]
public void CompiledWhenShouldResetPositionWhenFalse()
{
var evenIntegers = ZeroOrOne(Literals.Integer().When(x => x % 2 == 0)).And(Literals.Integer()).Compile();
var evenIntegers = ZeroOrOne(Literals.Integer().When((c, x) => x % 2 == 0)).And(Literals.Integer()).Compile();

Assert.True(evenIntegers.TryParse("1235", out var result1));
Assert.Equal(1235, result1.Item2);
}

[Fact]
public void CompiledIfShouldNotInvokeParserWhenFalse()
{
bool invoked = false;

var evenState = If(predicate: (context, x) => x % 2 == 0, state: 0, parser: Literals.Integer().Then(x => invoked = true)).Compile();
var oddState = If(predicate: (context, x) => x % 2 == 0, state: 1, parser: Literals.Integer().Then(x => invoked = true)).Compile();

Assert.False(oddState.TryParse("1234", out var result1));
Assert.False(invoked);

Assert.True(evenState.TryParse("1234", out var result2));
Assert.True(invoked);
}

[Fact]
public void ErrorShouldThrowIfParserSucceeds()
{
Expand Down
20 changes: 17 additions & 3 deletions test/Parlot.Tests/FluentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Xml;
using Xunit;

using static Parlot.Fluent.Parsers;
Expand All @@ -15,7 +14,7 @@ public class FluentTests
[Fact]
public void WhenShouldFailParserWhenFalse()
{
var evenIntegers = Literals.Integer().When(x => x % 2 == 0);
var evenIntegers = Literals.Integer().When((c, x) => x % 2 == 0);

Assert.True(evenIntegers.TryParse("1234", out var result1));
Assert.Equal(1234, result1);
Expand All @@ -24,10 +23,25 @@ public void WhenShouldFailParserWhenFalse()
Assert.Equal(default, result2);
}

[Fact]
public void IfShouldNotInvokeParserWhenFalse()
{
bool invoked = false;

var evenState = If(predicate: (context, x) => x % 2 == 0, state: 0, parser: Literals.Integer().Then(x => invoked = true));
var oddState = If(predicate: (context, x) => x % 2 == 0, state: 1, parser: Literals.Integer().Then(x => invoked = true));

Assert.False(oddState.TryParse("1234", out var result1));
Assert.False(invoked);

Assert.True(evenState.TryParse("1234", out var result2));
Assert.True(invoked);
}

[Fact]
public void WhenShouldResetPositionWhenFalse()
{
var evenIntegers = ZeroOrOne(Literals.Integer().When(x => x % 2 == 0)).And(Literals.Integer());
var evenIntegers = ZeroOrOne(Literals.Integer().When((c, x) => x % 2 == 0)).And(Literals.Integer());

Assert.True(evenIntegers.TryParse("1235", out var result1));
Assert.Equal(1235, result1.Item2);
Expand Down

0 comments on commit e370f53

Please sign in to comment.