diff --git a/README.md b/README.md
index 84a278e9..be1e510e 100644
--- a/README.md
+++ b/README.md
@@ -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:
diff --git a/src/Parlot/Parlot.csproj b/src/Parlot/Parlot.csproj
index 13f81b97..cda3c6a3 100644
--- a/src/Parlot/Parlot.csproj
+++ b/src/Parlot/Parlot.csproj
@@ -24,5 +24,21 @@
+
+
+
+
+
+ True
+ True
+ ParserOperatorExtensions.tt
+
+
+
+
+ TextTemplatingFileGenerator
+ ParserOperatorExtensions.cs
+
+
diff --git a/src/Parlot/ParserOperatorExtensions.cs b/src/Parlot/ParserOperatorExtensions.cs
new file mode 100644
index 00000000..b66a7db5
--- /dev/null
+++ b/src/Parlot/ParserOperatorExtensions.cs
@@ -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(Parser)
+ {
+ [OverloadResolutionPriority(-1)]
+ public static Sequence operator +(Parser p1, Parser p2)
+ {
+ return p1.And(p2);
+ }
+ }
+
+ extension(Sequence)
+ {
+ public static Sequence operator +(Sequence p1, IParser p2)
+ {
+ return p2 is Parser parser?
+ p1.And(parser) :
+ p1.And(new IParserAdapter(p2));
+ }
+ }
+
+ extension(Sequence)
+ {
+ public static Sequence operator +(Sequence p1, IParser p2)
+ {
+ return p2 is Parser parser?
+ p1.And(parser) :
+ p1.And(new IParserAdapter(p2));
+ }
+ }
+
+ extension(Sequence)
+ {
+ public static Sequence operator +(Sequence p1, IParser p2)
+ {
+ return p2 is Parser parser?
+ p1.And(parser) :
+ p1.And(new IParserAdapter(p2));
+ }
+ }
+
+ extension(Sequence)
+ {
+ public static Sequence operator +(Sequence p1, IParser p2)
+ {
+ return p2 is Parser parser?
+ p1.And(parser) :
+ p1.And(new IParserAdapter(p2));
+ }
+ }
+
+ extension(Sequence)
+ {
+ public static Sequence operator +(Sequence p1, IParser p2)
+ {
+ return p2 is Parser parser?
+ p1.And(parser) :
+ p1.And(new IParserAdapter(p2));
+ }
+ }
+
+ // | operator to replace Or method
+
+ extension(IParser)
+ {
+ public static OneOf operator |(IParser p1, IParser p2)
+ {
+ return new([new IParserAdapter(p1), new IParserAdapter(p2)]);
+ }
+ }
+
+ extension(OneOf)
+ {
+ public static OneOf operator |(OneOf p1, IParser p2)
+ {
+ return p2 is Parser parser ?
+ new OneOf([.. p1.OriginalParsers, parser]) :
+ new([.. p1.OriginalParsers, new IParserAdapter(p2)]);
+ }
+ }
+}
diff --git a/src/Parlot/ParserOperatorExtensions.tt b/src/Parlot/ParserOperatorExtensions.tt
new file mode 100644
index 00000000..8d3227b2
--- /dev/null
+++ b/src/Parlot/ParserOperatorExtensions.tt
@@ -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(Parser)
+ {
+ [OverloadResolutionPriority(-1)]
+ public static Sequence operator +(Parser p1, Parser 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> p2)
+ {
+ return p2 is Parser> parser?
+ p1.And(parser) :
+ p1.And(new IParserAdapter>(p2));
+ }
+ }
+<#
+ }
+#>
+
+ // + operator to replace Or method
+
+ extension(IParser)
+ {
+ public static OneOf operator |(IParser p1, IParser p2)
+ {
+ return new([new IParserAdapter(p1), new IParserAdapter(p2)]);
+ }
+ }
+
+ extension(OneOf)
+ {
+ public static OneOf operator |(OneOf p1, IParser p2)
+ {
+ return p2 is Parser parser ?
+ new OneOf([.. p1.OriginalParsers, parser]) :
+ new([.. p1.OriginalParsers, new IParserAdapter(p2)]);
+ }
+ }
+}
diff --git a/test/Parlot.Tests/Calc/FluentParserTests.cs b/test/Parlot.Tests/Calc/FluentParserTests.cs
index 915b7c58..f7447006 100644
--- a/test/Parlot.Tests/Calc/FluentParserTests.cs
+++ b/test/Parlot.Tests/Calc/FluentParserTests.cs
@@ -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();
}
}
diff --git a/test/Parlot.Tests/OperatorsTests.cs b/test/Parlot.Tests/OperatorsTests.cs
new file mode 100644
index 00000000..3698c2d2
--- /dev/null
+++ b/test/Parlot.Tests/OperatorsTests.cs
@@ -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 parser1 = Literals.Char('a');
+ Parser 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(result.Item1);
+ Assert.IsType(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(resultA);
+ Assert.True(successB);
+ Assert.IsType(resultB);
+
+ var parser2 = dogParser | animalParser;
+ successA = parser.TryParse("a", out resultA);
+ successB = parser.TryParse("b", out resultB);
+ Assert.True(successA);
+ Assert.IsType(resultA);
+ Assert.True(successB);
+ Assert.IsType(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();
+
+ var jsonArray =
+ Between(LBracket, Separated(Comma, json), RBracket)
+ .Then(static els => new JsonArray(els));
+
+ var jsonMember =
+ (String + Colon + json).Then(static member => new KeyValuePair(member.Item1.ToString(), member.Item3));
+
+ var jsonObject =
+ (LBrace + Separated(Comma, jsonMember) + RBrace)
+ .Then(static kvps => new JsonObject(new Dictionary(kvps.Item2)));
+
+ var Json = json.Parser = jsonString.Then() | jsonArray |jsonObject;
+
+ var input = "{\"name\":\"John\",\"age\":\"30\",\"cars\":[\"Ford\",\"BMW\",\"Fiat\"]}";
+ var success = Json.TryParse(input, out var result);
+ Assert.True(success);
+
+ }
+}