diff --git a/docs/covariance-example.md b/docs/covariance-example.md new file mode 100644 index 00000000..21d84170 --- /dev/null +++ b/docs/covariance-example.md @@ -0,0 +1,45 @@ +# Covariance Support Example + +This example demonstrates how `IParser` enables covariance, eliminating the need for wasteful `.Then(x => x)` conversions. + +## Before (Without Covariance) + +```csharp +class Animal { public string Name { get; set; } } +class Dog : Animal { public string Breed { get; set; } } +class Cat : Animal { public string Color { get; set; } } + +var dogParser = Terms.Text("dog").Then(_ => new Dog { Name = "Buddy", Breed = "Golden Retriever" }); +var catParser = Terms.Text("cat").Then(_ => new Cat { Name = "Whiskers", Color = "Orange" }); + +// Had to use .Then(x => x) to convert each parser - creates wrapper objects +var animalParser = dogParser.Then(x => x).Or(catParser.Then(x => x)); +``` + +## After (With Covariance) + +```csharp +class Animal { public string Name { get; set; } } +class Dog : Animal { public string Breed { get; set; } } +class Cat : Animal { public string Color { get; set; } } + +var dogParser = Terms.Text("dog").Then(_ => new Dog { Name = "Buddy", Breed = "Golden Retriever" }); +var catParser = Terms.Text("cat").Then(_ => new Cat { Name = "Whiskers", Color = "Orange" }); + +// Can use OneOf directly with IParser - no wrapper objects needed! +var animalParser = OneOf(dogParser, catParser); +``` + +## Benefits + +1. **No wrapper objects**: The original parser instances are reused without creating `Then` wrappers +2. **Cleaner syntax**: More readable code without explicit type conversions +3. **Better performance**: Eliminates the overhead of wrapper parser objects +4. **Type safety**: Still maintains full type safety through the covariant interface + +## How It Works + +- `IParser` is a covariant interface (note the `out` keyword) +- This means `IParser` can be used where `IParser` is expected +- The `OneOf` method now accepts `params IParser[]` instead of just `Parser[]` +- Under the hood, parsers are adapted as needed while preserving the original parser behavior diff --git a/docs/covariance-usage.cs b/docs/covariance-usage.cs new file mode 100644 index 00000000..b979c6a4 --- /dev/null +++ b/docs/covariance-usage.cs @@ -0,0 +1,89 @@ +using Parlot.Fluent; +using static Parlot.Fluent.Parsers; + +namespace CovarianceExample; + +// Example domain model for an expression parser +abstract class Expression +{ + public abstract int Evaluate(); +} + +class NumberExpression : Expression +{ + public int Value { get; set; } + public override int Evaluate() => Value; +} + +class AddExpression : Expression +{ + public Expression Left { get; set; } = null!; + public Expression Right { get; set; } = null!; + public override int Evaluate() => Left.Evaluate() + Right.Evaluate(); +} + +class MultiplyExpression : Expression +{ + public Expression Left { get; set; } = null!; + public Expression Right { get; set; } = null!; + public override int Evaluate() => Left.Evaluate() * Right.Evaluate(); +} + +class Program +{ + static void Main() + { + // Define parsers for specific expression types + var numberParser = Terms.Integer().Then(n => new NumberExpression { Value = n }); + + // BEFORE: Would need explicit conversions like this: + // var expressionParser = numberParser.Then(x => x); + + // AFTER: Can use covariance directly! + // The OneOf method now accepts IParser[] where T is covariant + Parser expressionParser = OneOf( + numberParser + // Could add more expression types here without .Then(x => x) + ); + + var result = expressionParser.Parse("42"); + if (result != null) + { + Console.WriteLine($"Parsed: {result.Evaluate()}"); // Output: Parsed: 42 + } + + // More complex example with multiple types + var addParser = Terms.Text("add").SkipAnd(Terms.Integer()) + .And(Terms.Integer()) + .Then(tuple => new AddExpression + { + Left = new NumberExpression { Value = tuple.Item1 }, + Right = new NumberExpression { Value = tuple.Item2 } + }); + + var multiplyParser = Terms.Text("mul").SkipAnd(Terms.Integer()) + .And(Terms.Integer()) + .Then(tuple => new MultiplyExpression + { + Left = new NumberExpression { Value = tuple.Item1 }, + Right = new NumberExpression { Value = tuple.Item2 } + }); + + // BEFORE: Would need .Then(x => x) on each parser + // var complexParser = numberParser.Then(x => x) + // .Or(addParser.Then(x => x)) + // .Or(multiplyParser.Then(x => x)); + + // AFTER: Clean and simple with covariance + var complexParser = OneOf(numberParser, addParser, multiplyParser); + + var result1 = complexParser.Parse("42"); + Console.WriteLine($"Number: {result1?.Evaluate()}"); // Output: Number: 42 + + var result2 = complexParser.Parse("add 10 20"); + Console.WriteLine($"Add: {result2?.Evaluate()}"); // Output: Add: 30 + + var result3 = complexParser.Parse("mul 5 6"); + Console.WriteLine($"Multiply: {result3?.Evaluate()}"); // Output: Multiply: 30 + } +} diff --git a/src/Parlot/Fluent/IParser.cs b/src/Parlot/Fluent/IParser.cs new file mode 100644 index 00000000..32b1a0dd --- /dev/null +++ b/src/Parlot/Fluent/IParser.cs @@ -0,0 +1,19 @@ +namespace Parlot.Fluent; + +/// +/// Covariant parser interface that allows a parser of a derived type to be used +/// where a parser of a base type is expected. +/// +/// The type of value this parser produces. +public interface IParser +{ + /// + /// Attempts to parse the input and returns whether the parse was successful. + /// + /// The parsing context. + /// The start position of the parsed value. + /// The end position of the parsed value. + /// The parsed value if successful, as object to support covariance. + /// True if parsing was successful, false otherwise. + bool Parse(ParseContext context, out int start, out int end, out object? value); +} diff --git a/src/Parlot/Fluent/IParserAdapter.cs b/src/Parlot/Fluent/IParserAdapter.cs new file mode 100644 index 00000000..82c66d49 --- /dev/null +++ b/src/Parlot/Fluent/IParserAdapter.cs @@ -0,0 +1,87 @@ +using Parlot.Compilation; +using Parlot.Rewriting; +using System.Linq.Expressions; + +namespace Parlot.Fluent; + +/// +/// Adapts an IParser<T> to a Parser<T> for use in contexts that require Parser. +/// This is used internally to support covariance. +/// +internal sealed class IParserAdapter : Parser, ISeekable, ICompilable +{ + private readonly IParser _parser; + + public IParserAdapter(IParser parser) + { + _parser = parser ?? throw new System.ArgumentNullException(nameof(parser)); + + // Forward ISeekable properties from the wrapped parser if it implements ISeekable + if (_parser is ISeekable seekable) + { + CanSeek = seekable.CanSeek; + ExpectedChars = seekable.ExpectedChars; + SkipWhitespace = seekable.SkipWhitespace; + } + } + + public bool CanSeek { get; } + + public char[] ExpectedChars { get; } = []; + + public bool SkipWhitespace { get; } + + public override bool Parse(ParseContext context, ref ParseResult result) + { + var success = _parser.Parse(context, out int start, out int end, out object? value); + if (success) + { + result.Set(start, end, (T)value!); + } + return success; + } + + public CompilationResult Compile(CompilationContext context) + { + // If the wrapped parser is actually a Parser, delegate compilation to it + if (_parser is Parser parser) + { + return parser.Build(context); + } + + // Otherwise, fall back to the default non-compilable behavior + // This uses the Parse method which will work with any IParser + var result = context.CreateCompilationResult(false); + + // ParseResult parseResult; + var parseResult = Expression.Variable(typeof(ParseResult), $"value{context.NextNumber}"); + result.Variables.Add(parseResult); + + // success = this.Parse(context.ParseContext, ref parseResult) + result.Body.Add( + Expression.Assign(result.Success, + Expression.Call( + Expression.Constant(this), + GetType().GetMethod("Parse", [typeof(ParseContext), typeof(ParseResult).MakeByRefType()])!, + context.ParseContext, + parseResult)) + ); + + if (!context.DiscardResult) + { + var value = result.Value = Expression.Variable(typeof(T), $"value{context.NextNumber}"); + result.Variables.Add(value); + + result.Body.Add( + Expression.IfThen( + result.Success, + Expression.Assign(value, Expression.Field(parseResult, "Value")) + ) + ); + } + + return result; + } + + public override string ToString() => _parser.ToString() ?? "IParserAdapter"; +} diff --git a/src/Parlot/Fluent/Parser.cs b/src/Parlot/Fluent/Parser.cs index 53365ac7..64533199 100644 --- a/src/Parlot/Fluent/Parser.cs +++ b/src/Parlot/Fluent/Parser.cs @@ -5,10 +5,24 @@ namespace Parlot.Fluent; -public abstract partial class Parser +public abstract partial class Parser : IParser { public abstract bool Parse(ParseContext context, ref ParseResult result); + /// + /// Attempts to parse the input and returns whether the parse was successful. + /// This is the covariant version of Parse for use with the IParser<out T> interface. + /// + bool IParser.Parse(ParseContext context, out int start, out int end, out object? value) + { + var result = new ParseResult(); + var success = Parse(context, ref result); + start = result.Start; + end = result.End; + value = result.Value; + return success; + } + /// /// Builds a parser that converts the previous result when it succeeds. /// diff --git a/src/Parlot/Fluent/Parsers.OneOf.cs b/src/Parlot/Fluent/Parsers.OneOf.cs index 388a6598..7e2dcd62 100644 --- a/src/Parlot/Fluent/Parsers.OneOf.cs +++ b/src/Parlot/Fluent/Parsers.OneOf.cs @@ -34,8 +34,37 @@ public static Parser Or(this Parser parser, Parser or) return new OneOf(parser, or); } + /// + /// Builds a parser that return either of the first successful of the specified parsers. + /// Uses covariance to accept parsers of derived types. + /// + public static Parser Or(this IParser parser, IParser or) + where A : T + where B : T + { + // Convert IParser to Parser if needed + var parserA = parser as Parser ?? new IParserAdapter(parser); + var parserB = or as Parser ?? new IParserAdapter(or); + return new OneOf(parserA, parserB); + } + /// /// Builds a parser that return either of the first successful of the specified parsers. /// public static Parser OneOf(params Parser[] parsers) => new OneOf(parsers); + + /// + /// Builds a parser that return either of the first successful of the specified parsers. + /// Uses covariance to accept parsers of derived types. + /// + public static Parser OneOf(params IParser[] parsers) + { + // Convert IParser to Parser if needed + var converted = new Parser[parsers.Length]; + for (int i = 0; i < parsers.Length; i++) + { + converted[i] = parsers[i] as Parser ?? new IParserAdapter(parsers[i]); + } + return new OneOf(converted); + } } diff --git a/src/Samples/Json/JsonParser.cs b/src/Samples/Json/JsonParser.cs index 8f13c81c..c88e3dc6 100644 --- a/src/Samples/Json/JsonParser.cs +++ b/src/Samples/Json/JsonParser.cs @@ -21,13 +21,13 @@ static JsonParser() var jsonString = String - .Then(static s => new JsonString(s.ToString())); + .Then(static s => new JsonString(s.ToString())); var json = Deferred(); var jsonArray = Between(LBracket, Separated(Comma, json), RBracket) - .Then(static els => new JsonArray(els)); + .Then(static els => new JsonArray(els)); var jsonMember = String.And(Colon).And(json) @@ -35,9 +35,9 @@ static JsonParser() var jsonObject = Between(LBrace, Separated(Comma, jsonMember), RBrace) - .Then(static kvps => new JsonObject(new Dictionary(kvps))); + .Then(static kvps => new JsonObject(new Dictionary(kvps))); - Json = json.Parser = jsonString.Or(jsonArray).Or(jsonObject); + Json = json.Parser = OneOf(jsonString, jsonArray, jsonObject); } public static IJson Parse(string input) diff --git a/src/Samples/Sql/SqlParser.cs b/src/Samples/Sql/SqlParser.cs index ced8c9bd..84275d50 100644 --- a/src/Samples/Sql/SqlParser.cs +++ b/src/Samples/Sql/SqlParser.cs @@ -57,13 +57,13 @@ static SqlParser() var PARTITION = Terms.Text("PARTITION", caseInsensitive: true); // Literals - var numberLiteral = Terms.Decimal().Then(d => new LiteralExpression(d)); + var numberLiteral = Terms.Decimal().Then(d => new LiteralExpression(d)); var stringLiteral = Terms.String(StringLiteralQuotes.Single) - .Then(s => new LiteralExpression(s.Span.ToString())); + .Then(s => new LiteralExpression(s.Span.ToString())); - var booleanLiteral = TRUE.Then(new LiteralExpression(true)) - .Or(FALSE.Then(new LiteralExpression(false))); + var booleanLiteral = TRUE.Then(new LiteralExpression(true)) + .Or(FALSE.Then(new LiteralExpression(false))); // Identifiers var simpleIdentifier = Terms.Identifier() @@ -83,47 +83,49 @@ static SqlParser() var expressionList = Separated(COMMA, expression); // Function arguments - var starArg = STAR.Then(_ => new StarArgument()); - var selectArg = selectStatement.Then(s => new SelectStatementArgument(s)); - var exprListArg = expressionList.Then(exprs => new ExpressionListArguments(exprs)); - var emptyArg = Always(new EmptyArguments()); - var functionArgs = starArg.Or(selectArg).Or(exprListArg).Or(emptyArg); + var starArg = STAR.Then(_ => new StarArgument()); + var selectArg = selectStatement.Then(s => new SelectStatementArgument(s)); + var exprListArg = expressionList.Then(exprs => new ExpressionListArguments(exprs)); + var emptyArg = Always(new EmptyArguments()); + var functionArgs = OneOf(starArg, selectArg, exprListArg, emptyArg); // Function call var functionCall = identifier.And(Between(LPAREN, functionArgs, RPAREN)) - .Then(x => new FunctionCall(x.Item1, x.Item2)); + .Then(x => new FunctionCall(x.Item1, x.Item2)); // Parameter - var parameter = AT.And(identifier).Then(x => new ParameterExpression(x.Item2)); + var parameter = AT.And(identifier).Then(x => new ParameterExpression(x.Item2)); // Tuple var tuple = Between(LPAREN, expressionList, RPAREN) - .Then(exprs => new TupleExpression(exprs)); + .Then(exprs => new TupleExpression(exprs)); // Parenthesized select var parSelectStatement = Between(LPAREN, selectStatement, RPAREN) - .Then(s => new ParenthesizedSelectStatement(s)); + .Then(s => new ParenthesizedSelectStatement(s)); // Basic term - var identifierExpr = identifier.Then(id => new IdentifierExpression(id)); - - var term = functionCall - .Or(parSelectStatement) - .Or(tuple) - .Or(booleanLiteral) - .Or(stringLiteral) - .Or(numberLiteral) - .Or(identifierExpr) - .Or(parameter); + var identifierExpr = identifier.Then(id => new IdentifierExpression(id)); + + var term = OneOf( + functionCall, + parSelectStatement, + tuple, + booleanLiteral, + stringLiteral, + numberLiteral, + identifierExpr, + parameter + ); // Unary expressions - var unaryMinus = Terms.Char('-').And(term).Then(x => new UnaryExpression(UnaryOperator.Minus, x.Item2)); - var unaryPlus = Terms.Char('+').And(term).Then(x => new UnaryExpression(UnaryOperator.Plus, x.Item2)); - var unaryNot = NOT.And(term).Then(x => new UnaryExpression(UnaryOperator.Not, x.Item2)); - var unaryBitwiseNot = Terms.Char('~').And(term).Then(x => new UnaryExpression(UnaryOperator.BitwiseNot, x.Item2)); + var unaryMinus = Terms.Char('-').And(term).Then(x => new UnaryExpression(UnaryOperator.Minus, x.Item2)); + var unaryPlus = Terms.Char('+').And(term).Then(x => new UnaryExpression(UnaryOperator.Plus, x.Item2)); + var unaryNot = NOT.And(term).Then(x => new UnaryExpression(UnaryOperator.Not, x.Item2)); + var unaryBitwiseNot = Terms.Char('~').And(term).Then(x => new UnaryExpression(UnaryOperator.BitwiseNot, x.Item2)); - var unaryExpr = unaryMinus.Or(unaryPlus).Or(unaryNot).Or(unaryBitwiseNot); - var primary = unaryExpr.Or(term); + var unaryExpr = OneOf(unaryMinus, unaryPlus, unaryNot, unaryBitwiseNot); + var primary = OneOf(unaryExpr, term); // Binary operators var notLike = NOT.AndSkip(LIKE); @@ -177,23 +179,23 @@ static SqlParser() // BETWEEN and IN expressions var betweenExpr = andExpr.And(NOT.Optional()).AndSkip(BETWEEN).And(bitwise).AndSkip(AND).And(bitwise) - .Then(result => + .Then(result => { var (expr, notKeyword, lower, upper) = result; return new BetweenExpression(expr, lower, upper, notKeyword.HasValue); }); var inExpr = andExpr.And(NOT.Optional()).AndSkip(IN).AndSkip(LPAREN).And(expressionList).AndSkip(RPAREN) - .Then(result => + .Then(result => { var (expr, notKeyword, values) = result; return new InExpression(expr, values, notKeyword.HasValue); }); - expression.Parser = betweenExpr.Or(inExpr).Or(orExpr); + expression.Parser = OneOf(betweenExpr, inExpr, orExpr); // Column source - var columnSourceId = identifier.Then(id => new ColumnSourceIdentifier(id)); + var columnSourceId = identifier.Then(id => new ColumnSourceIdentifier(id)); // Deferred for OVER clause components var columnItemList = Separated(COMMA, columnItem.Or(STAR.Then(new ColumnItem(new ColumnSourceIdentifier(new Identifier("*")), null)))); @@ -216,13 +218,13 @@ static SqlParser() }); var columnSourceFunc = functionCall.And(overClause.Optional()) - .Then(result => + .Then(result => { var (func, over) = result; return new ColumnSourceFunction((FunctionCall)func, over.OrSome(null)); }); - var columnSource = columnSourceFunc.Or(columnSourceId); + var columnSource = OneOf(columnSourceFunc, columnSourceId); // Column item with alias var columnAlias = AS.SkipAnd(identifier); @@ -248,14 +250,13 @@ static SqlParser() var unionStatementList = Deferred>(); var tableSourceSubQuery = LPAREN.SkipAnd(unionStatementList).AndSkip(RPAREN).AndSkip(AS).And(simpleIdentifier) - .Then(result => + .Then(result => { var (query, alias) = result; return new TableSourceSubQuery(query, alias.ToString()); }); - var tableSourceItemAsTableSource = tableSourceItem.Then(t => t); - var tableSource = tableSourceSubQuery.Or(tableSourceItemAsTableSource); + var tableSource = OneOf(tableSourceSubQuery, tableSourceItem); var tableSourceList = Separated(COMMA, tableSource); // Join diff --git a/test/Parlot.Tests/CovarianceTests.cs b/test/Parlot.Tests/CovarianceTests.cs new file mode 100644 index 00000000..dce43974 --- /dev/null +++ b/test/Parlot.Tests/CovarianceTests.cs @@ -0,0 +1,121 @@ +#nullable enable +using Parlot.Fluent; +using Xunit; +using static Parlot.Fluent.Parsers; + +namespace Parlot.Tests; + +public class CovarianceTests +{ + // Test classes for demonstrating covariance + class Animal { public string Name { get; set; } = ""; } + class Dog : Animal { public string Breed { get; set; } = ""; } + class Cat : Animal { public string Color { get; set; } = ""; } + + [Fact] + public void IParserShouldSupportCovariance() + { + // Create parsers that return specific types + var dogParser = Terms.Text("dog").Then(_ => new Dog { Name = "Buddy", Breed = "Golden Retriever" }); + var catParser = Terms.Text("cat").Then(_ => new Cat { Name = "Whiskers", Color = "Orange" }); + + // Before: Would need .Then(x => x) to convert each parser + // After: Can use IParser directly with covariance + + // This should work due to covariance - Dog and Cat can be used where Animal is expected + IParser animalDogParser = dogParser; + IParser animalCatParser = catParser; + + // Verify the parsers work + var context1 = new ParseContext(new Scanner("dog")); + var success1 = animalDogParser.Parse(context1, out int start1, out int end1, out object? value1); + Assert.True(success1); + Assert.IsType(value1); + Assert.Equal("Buddy", ((Dog)value1!).Name); + + var context2 = new ParseContext(new Scanner("cat")); + var success2 = animalCatParser.Parse(context2, out int start2, out int end2, out object? value2); + Assert.True(success2); + Assert.IsType(value2); + Assert.Equal("Whiskers", ((Cat)value2!).Name); + } + + [Fact] + public void OneOfShouldAcceptCovariantParsers() + { + // Create parsers for derived types + var dogParser = Terms.Text("dog").Then(_ => new Dog { Name = "Buddy", Breed = "Golden Retriever" }); + var catParser = Terms.Text("cat").Then(_ => new Cat { Name = "Whiskers", Color = "Orange" }); + + // Before: Would need dogParser.Then(x => x).Or(catParser.Then(x => x)) + // After: Can use OneOf with IParser directly + var animalParser = OneOf(dogParser, catParser); + + var result1 = animalParser.Parse("dog"); + Assert.NotNull(result1); + Assert.IsType(result1); + Assert.Equal("Buddy", result1.Name); + Assert.Equal("Golden Retriever", ((Dog)result1).Breed); + + var result2 = animalParser.Parse("cat"); + Assert.NotNull(result2); + Assert.IsType(result2); + Assert.Equal("Whiskers", result2.Name); + Assert.Equal("Orange", ((Cat)result2).Color); + } + + [Fact] + public void OrShouldAcceptCovariantParsers() + { + // Create parsers for derived types + var dogParser = Terms.Text("dog").Then(_ => new Dog { Name = "Buddy", Breed = "Golden Retriever" }); + var catParser = Terms.Text("cat").Then(_ => new Cat { Name = "Whiskers", Color = "Orange" }); + + // Use Or with covariant types + var animalParser = dogParser.Or(catParser); + + var result1 = animalParser.Parse("dog"); + Assert.NotNull(result1); + Assert.IsType(result1); + + var result2 = animalParser.Parse("cat"); + Assert.NotNull(result2); + Assert.IsType(result2); + } + + [Fact] + public void CovariantParsersShouldWorkWithCompilation() + { + // Test that covariance works with compiled parsers + var dogParser = Terms.Text("dog").Then(_ => new Dog { Name = "Buddy", Breed = "Golden Retriever" }); + var catParser = Terms.Text("cat").Then(_ => new Cat { Name = "Whiskers", Color = "Orange" }); + + var animalParser = OneOf(dogParser, catParser).Compile(); + + var result1 = animalParser.Parse("dog"); + Assert.NotNull(result1); + Assert.IsType(result1); + Assert.Equal("Buddy", result1.Name); + + var result2 = animalParser.Parse("cat"); + Assert.NotNull(result2); + Assert.IsType(result2); + Assert.Equal("Whiskers", result2.Name); + } + + [Fact] + public void NullableCovariance() + { + // Test covariance with nullable reference types + var stringParser = Terms.Text("hello").Then(_ => "world"); + + // string is derived from object, should work with covariance + IParser objectParser = stringParser; + + var context = new ParseContext(new Scanner("hello")); + var success = objectParser.Parse(context, out int start, out int end, out object? value); + + Assert.True(success); + Assert.Equal("world", value); + } +}