From c9559b4ddc202744cb78db419daa24bb3047cf90 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Fri, 14 Nov 2025 16:50:54 -0800 Subject: [PATCH] Add c# operators support --- README.md | 17 +++ src/Parlot/Parlot.csproj | 16 +++ src/Parlot/ParserOperatorExtensions.cs | 113 +++++++++++++++ src/Parlot/ParserOperatorExtensions.tt | 82 +++++++++++ test/Parlot.Tests/Calc/FluentParserTests.cs | 2 +- test/Parlot.Tests/OperatorsTests.cs | 146 ++++++++++++++++++++ 6 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 src/Parlot/ParserOperatorExtensions.cs create mode 100644 src/Parlot/ParserOperatorExtensions.tt create mode 100644 test/Parlot.Tests/OperatorsTests.cs 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); + + } +}