Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,23 @@ The benchmarks were executed with the following versions:
- Superpower 3.0.0
- Newtonsoft.Json 13.0.3

### Operator Syntax

Parlot supports intuitive operators for parser composition:

- **`+` operator**: Combines parsers in sequence (alternative to `.And()`)
- **`|` operator**: Creates choice between parsers (alternative to `.Or()`)

```c#
// Using operators
var parser = Literals.Char('a') + Literals.Char('b') + Literals.Char('c');
var choice = Literals.Char('x') | Literals.Char('y') | Literals.Char('z');

// Equivalent to
var parser = Literals.Char('a').And(Literals.Char('b')).And(Literals.Char('c'));
var choice = Literals.Char('x').Or(Literals.Char('y')).Or(Literals.Char('z'));
```

### Usages

Parlot is already used in these projects:
Expand Down
16 changes: 16 additions & 0 deletions src/Parlot/Parlot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,21 @@
<ItemGroup>
<None Include="buildTransitive\Parlot.props" Pack="true" PackagePath="buildTransitive" />
</ItemGroup>
<ItemGroup>
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
</ItemGroup>
<ItemGroup>
<Compile Update="ParserOperatorExtensions.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>ParserOperatorExtensions.tt</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="ParserOperatorExtensions.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>ParserOperatorExtensions.cs</LastGenOutput>
</None>
</ItemGroup>

</Project>
113 changes: 113 additions & 0 deletions src/Parlot/ParserOperatorExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@

using Parlot.Fluent;
using System.Runtime.CompilerServices;

namespace Parlot;

// Other operators that could be overloaded but are not:
// & (bitwise and) could be used for AndAlso (logical and)
// ^ (bitwise xor) could be used for exclusive Or
// ~ (bitwise not) could be used for Not
// + (unary plus) could be used for something like "at least one"
// - (unary minus) could be used for something like "optional"
// ++ (increment) could be used for "one or more"
// -- (decrement) could be used for "zero or more"
// == (equality) could be used for "equals"
// != (inequality) could be used for "not equals"
// - (subtraction) could be used for "except"
// * (multiplication) could be used for "repeat n times"
// / (division) could be used for "divide into n parts"
// % (modulus) could be used for "repeat until condition met"
// << (left shift) could be used for "lookahead"
// >> (right shift) could be used for "lookbehind"
// >>> (unsigned right shift) could be used for "skip n characters"
// ! (logical not) could be used for "not"
// ? (ternary conditional) could be used for "if-then-else"
// >= (greater than or equal to) could be used for "at least n times"
// <= (less than or equal to) could be used for "at most n times"
// < (less than) could be used for "less than n times"
// > (greater than) could be used for "more than n times"

public static partial class ParserOperatorExtensions
{
// + operator to replace And method

extension<T1, T2>(Parser<T1>)
{
[OverloadResolutionPriority(-1)]
public static Sequence<T1, T2> operator +(Parser<T1> p1, Parser<T2> p2)
{
return p1.And(p2);
}
}

extension<T1, T2, T3>(Sequence<T1, T2>)
{
public static Sequence<T1, T2, T3> operator +(Sequence<T1, T2> p1, IParser<T3> p2)
{
return p2 is Parser<T3> parser?
p1.And(parser) :
p1.And(new IParserAdapter<T3>(p2));
}
}

extension<T1, T2, T3, T4>(Sequence<T1, T2, T3>)
{
public static Sequence<T1, T2, T3, T4> operator +(Sequence<T1, T2, T3> p1, IParser<T4> p2)
{
return p2 is Parser<T4> parser?
p1.And(parser) :
p1.And(new IParserAdapter<T4>(p2));
}
}

extension<T1, T2, T3, T4, T5>(Sequence<T1, T2, T3, T4>)
{
public static Sequence<T1, T2, T3, T4, T5> operator +(Sequence<T1, T2, T3, T4> p1, IParser<T5> p2)
{
return p2 is Parser<T5> parser?
p1.And(parser) :
p1.And(new IParserAdapter<T5>(p2));
}
}

extension<T1, T2, T3, T4, T5, T6>(Sequence<T1, T2, T3, T4, T5>)
{
public static Sequence<T1, T2, T3, T4, T5, T6> operator +(Sequence<T1, T2, T3, T4, T5> p1, IParser<T6> p2)
{
return p2 is Parser<T6> parser?
p1.And(parser) :
p1.And(new IParserAdapter<T6>(p2));
}
}

extension<T1, T2, T3, T4, T5, T6, T7>(Sequence<T1, T2, T3, T4, T5, T6>)
{
public static Sequence<T1, T2, T3, T4, T5, T6, T7> operator +(Sequence<T1, T2, T3, T4, T5, T6> p1, IParser<T7> p2)
{
return p2 is Parser<T7> parser?
p1.And(parser) :
p1.And(new IParserAdapter<T7>(p2));
}
}

// | operator to replace Or method

extension<T>(IParser<T>)
{
public static OneOf<T> operator |(IParser<T> p1, IParser<T> p2)
{
return new([new IParserAdapter<T>(p1), new IParserAdapter<T>(p2)]);
}
}

extension<T>(OneOf<T>)
{
public static OneOf<T> operator |(OneOf<T> p1, IParser<T> p2)
{
return p2 is Parser<T> parser ?
new OneOf<T>([.. p1.OriginalParsers, parser]) :
new([.. p1.OriginalParsers, new IParserAdapter<T>(p2)]);
}
}
}
82 changes: 82 additions & 0 deletions src/Parlot/ParserOperatorExtensions.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>

using Parlot.Fluent;
using System.Runtime.CompilerServices;

namespace Parlot;

// Other operators that could be overloaded but are not:
// & (bitwise and) could be used for AndAlso (logical and)
// ^ (bitwise xor) could be used for exclusive Or
// ~ (bitwise not) could be used for Not
// + (unary plus) could be used for something like "at least one"
// - (unary minus) could be used for something like "optional"
// ++ (increment) could be used for "one or more"
// -- (decrement) could be used for "zero or more"
// == (equality) could be used for "equals"
// != (inequality) could be used for "not equals"
// - (subtraction) could be used for "except"
// * (multiplication) could be used for "repeat n times"
// / (division) could be used for "divide into n parts"
// % (modulus) could be used for "repeat until condition met"
// << (left shift) could be used for "lookahead"
// >> (right shift) could be used for "lookbehind"
// >>> (unsigned right shift) could be used for "skip n characters"
// ! (logical not) could be used for "not"
// ? (ternary conditional) could be used for "if-then-else"
// >= (greater than or equal to) could be used for "at least n times"
// <= (less than or equal to) could be used for "at most n times"
// < (less than) could be used for "less than n times"
// > (greater than) could be used for "more than n times"

public static partial class ParserOperatorExtensions
{
// | operator to replace And method

extension<T1, T2>(Parser<T1>)
{
[OverloadResolutionPriority(-1)]
public static Sequence<T1, T2> operator +(Parser<T1> p1, Parser<T2> p2)
{
return p1.And(p2);
}
}
<#
for(var i = 3; i < 8; i++)
{
#>

extension<<#= string.Join(", ", Enumerable.Range(1, i).Select(x => "T" + x))#>>(Sequence<<#= string.Join(", ", Enumerable.Range(1, i - 1).Select(x => "T" + x))#>>)
{
public static Sequence<<#= string.Join(", ", Enumerable.Range(1, i).Select(x => "T" + x))#>> operator +(Sequence<<#= string.Join(", ", Enumerable.Range(1, i - 1).Select(x => "T" + x))#>> p1, IParser<T<#= i #>> p2)
{
return p2 is Parser<T<#= i #>> parser?
p1.And(parser) :
p1.And(new IParserAdapter<T<#= i #>>(p2));
}
}
<#
}
#>

// + operator to replace Or method

extension<T>(IParser<T>)
{
public static OneOf<T> operator |(IParser<T> p1, IParser<T> p2)
{
return new([new IParserAdapter<T>(p1), new IParserAdapter<T>(p2)]);
}
}

extension<T>(OneOf<T>)
{
public static OneOf<T> operator |(OneOf<T> p1, IParser<T> p2)
{
return p2 is Parser<T> parser ?
new OneOf<T>([.. p1.OriginalParsers, parser]) :
new([.. p1.OriginalParsers, new IParserAdapter<T>(p2)]);
}
}
}
2 changes: 1 addition & 1 deletion test/Parlot.Tests/Calc/FluentParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ public class FluentParserTests : CalcTests
{
protected override decimal Evaluate(string text)
{
FluentParser.Expression.TryParse(text, out var expression);
_ = FluentParser.Expression.TryParse(text, out var expression);
return expression.Evaluate();
}
}
146 changes: 146 additions & 0 deletions test/Parlot.Tests/OperatorsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using Parlot.Fluent;
using Parlot.Tests.Json;
using System.Collections.Generic;
using Xunit;

using static Parlot.Fluent.Parsers;


namespace Parlot.Tests;

public class OperatorsTests
{
[Fact]
public void Sequence_Operator_Plus_Works()
{
var parser = Literals.Char('a') + Literals.Char('b') + Literals.Char('c');
var success = parser.TryParse("abc", out var result);
Assert.True(success);
Assert.Equal(('a', 'b', 'c'), result);
}

[Fact]
public void Choice_Operator_Pipe_Works()
{
var parser = Literals.Char('a') | Literals.Char('b') | Literals.Char('c');
var successA = parser.TryParse("a", out var resultA);
var successB = parser.TryParse("b", out var resultB);
var successC = parser.TryParse("c", out var resultC);
var successD = parser.TryParse("d", out _);
Assert.True(successA);
Assert.Equal('a', resultA);
Assert.True(successB);
Assert.Equal('b', resultB);
Assert.True(successC);
Assert.Equal('c', resultC);
Assert.False(successD);
}

[Fact]
public void Choice_Operator_Pipe_Works_With_Mixed_Parsers()
{
IParser<char> parser1 = Literals.Char('a');
Parser<char> parser2 = Literals.Char('b');
var parser = parser1 | parser2 | Literals.Char('c');
var successA = parser.TryParse("a", out var resultA);
var successB = parser.TryParse("b", out var resultB);
var successC = parser.TryParse("c", out var resultC);
var successD = parser.TryParse("d", out _);
Assert.True(successA);
Assert.Equal('a', resultA);
Assert.True(successB);
Assert.Equal('b', resultB);
Assert.True(successC);
Assert.Equal('c', resultC);
Assert.False(successD);
}

[Fact]
public void CanMix_Operator_Pipe_And_Plus()
{
var choiceParser = Literals.Char('x') | Literals.Char('y');
var parser = Literals.Char('a') + Literals.Char('b') + choiceParser;
var successX = parser.TryParse("abx", out var resultX);
var successY = parser.TryParse("aby", out var resultY);
var successZ = parser.TryParse("abz", out _);
Assert.True(successX);
Assert.Equal(('a', 'b', 'x'), resultX);
Assert.True(successY);
Assert.Equal(('a', 'b', 'y'), resultY);
Assert.False(successZ);
}

[Fact]
public void Operator_Plus_Supports_Covariance()
{
var animalParser = Literals.Char('a').Then(c => new Animal());
var dogParser = Literals.Char('b').Then(c => new Dog());
var parser = animalParser + dogParser;
var success = parser.TryParse("ab", out var result);
Assert.True(success);
Assert.IsType<Animal>(result.Item1);
Assert.IsType<Dog>(result.Item2);
}

private class Animal { }
private class Dog : Animal { }

[Fact]
public void Operator_Pipe_Supports_Covariance()
{
var animalParser = Literals.Char('a').Then(c => new Animal());
var dogParser = Literals.Char('b').Then(c => new Dog());
var parser = animalParser | dogParser;
var successA = parser.TryParse("a", out var resultA);
var successB = parser.TryParse("b", out var resultB);
Assert.True(successA);
Assert.IsType<Animal>(resultA);
Assert.True(successB);
Assert.IsType<Dog>(resultB);

var parser2 = dogParser | animalParser;
successA = parser.TryParse("a", out resultA);
successB = parser.TryParse("b", out resultB);
Assert.True(successA);
Assert.IsType<Animal>(resultA);
Assert.True(successB);
Assert.IsType<Dog>(resultB);
}

[Fact]
public void Can_Parse_Json_With_Operators()
{
var LBrace = Terms.Char('{');
var RBrace = Terms.Char('}');
var LBracket = Terms.Char('[');
var RBracket = Terms.Char(']');
var Colon = Terms.Char(':');
var Comma = Terms.Char(',');

var String = Terms.String(StringLiteralQuotes.Double);

var jsonString =
String
.Then(static s => new JsonString(s.ToString()));

var json = Deferred<IJson>();

var jsonArray =
Between(LBracket, Separated(Comma, json), RBracket)
.Then(static els => new JsonArray(els));

var jsonMember =
(String + Colon + json).Then(static member => new KeyValuePair<string, IJson>(member.Item1.ToString(), member.Item3));

var jsonObject =
(LBrace + Separated(Comma, jsonMember) + RBrace)
.Then(static kvps => new JsonObject(new Dictionary<string, IJson>(kvps.Item2)));

var Json = json.Parser = jsonString.Then<IJson>() | jsonArray |jsonObject;

var input = "{\"name\":\"John\",\"age\":\"30\",\"cars\":[\"Ford\",\"BMW\",\"Fiat\"]}";
var success = Json.TryParse(input, out var result);
Assert.True(success);

}
}