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
45 changes: 45 additions & 0 deletions docs/covariance-example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Covariance Support Example

This example demonstrates how `IParser<out T>` enables covariance, eliminating the need for wasteful `.Then<TBase>(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<Animal>(x => x) to convert each parser - creates wrapper objects
var animalParser = dogParser.Then<Animal>(x => x).Or(catParser.Then<Animal>(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<T> - no wrapper objects needed!
var animalParser = OneOf<Animal>(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<out T>` is a covariant interface (note the `out` keyword)
- This means `IParser<Dog>` can be used where `IParser<Animal>` is expected
- The `OneOf<T>` method now accepts `params IParser<T>[]` instead of just `Parser<T>[]`
- Under the hood, parsers are adapted as needed while preserving the original parser behavior
89 changes: 89 additions & 0 deletions docs/covariance-usage.cs
Original file line number Diff line number Diff line change
@@ -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<Expression>(x => x);

// AFTER: Can use covariance directly!
// The OneOf method now accepts IParser<T>[] where T is covariant
Parser<Expression> expressionParser = OneOf<Expression>(
numberParser
// Could add more expression types here without .Then<Expression>(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<Expression>(x => x) on each parser
// var complexParser = numberParser.Then<Expression>(x => x)
// .Or(addParser.Then<Expression>(x => x))
// .Or(multiplyParser.Then<Expression>(x => x));

// AFTER: Clean and simple with covariance
var complexParser = OneOf<Expression>(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
}
}
19 changes: 19 additions & 0 deletions src/Parlot/Fluent/IParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Parlot.Fluent;

/// <summary>
/// Covariant parser interface that allows a parser of a derived type to be used
/// where a parser of a base type is expected.
/// </summary>
/// <typeparam name="T">The type of value this parser produces.</typeparam>
public interface IParser<out T>
{
/// <summary>
/// Attempts to parse the input and returns whether the parse was successful.
/// </summary>
/// <param name="context">The parsing context.</param>
/// <param name="start">The start position of the parsed value.</param>
/// <param name="end">The end position of the parsed value.</param>
/// <param name="value">The parsed value if successful, as object to support covariance.</param>
/// <returns>True if parsing was successful, false otherwise.</returns>
bool Parse(ParseContext context, out int start, out int end, out object? value);
}
87 changes: 87 additions & 0 deletions src/Parlot/Fluent/IParserAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using Parlot.Compilation;
using Parlot.Rewriting;
using System.Linq.Expressions;

namespace Parlot.Fluent;

/// <summary>
/// Adapts an IParser&lt;T&gt; to a Parser&lt;T&gt; for use in contexts that require Parser.
/// This is used internally to support covariance.
/// </summary>
internal sealed class IParserAdapter<T> : Parser<T>, ISeekable, ICompilable
{
private readonly IParser<T> _parser;

public IParserAdapter(IParser<T> 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<T> 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<T>, delegate compilation to it
if (_parser is Parser<T> 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<T>
var result = context.CreateCompilationResult<T>(false);

// ParseResult<T> parseResult;
var parseResult = Expression.Variable(typeof(ParseResult<T>), $"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<T>).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";
}
16 changes: 15 additions & 1 deletion src/Parlot/Fluent/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,24 @@

namespace Parlot.Fluent;

public abstract partial class Parser<T>
public abstract partial class Parser<T> : IParser<T>
{
public abstract bool Parse(ParseContext context, ref ParseResult<T> result);

/// <summary>
/// Attempts to parse the input and returns whether the parse was successful.
/// This is the covariant version of Parse for use with the IParser&lt;out T&gt; interface.
/// </summary>
bool IParser<T>.Parse(ParseContext context, out int start, out int end, out object? value)
{
var result = new ParseResult<T>();
var success = Parse(context, ref result);
start = result.Start;
end = result.End;
value = result.Value;
return success;
}

/// <summary>
/// Builds a parser that converts the previous result when it succeeds.
/// </summary>
Expand Down
29 changes: 29 additions & 0 deletions src/Parlot/Fluent/Parsers.OneOf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,37 @@ public static Parser<T> Or<A, B, T>(this Parser<A> parser, Parser<B> or)
return new OneOf<A, B, T>(parser, or);
}

/// <summary>
/// Builds a parser that return either of the first successful of the specified parsers.
/// Uses covariance to accept parsers of derived types.
/// </summary>
public static Parser<T> Or<A, B, T>(this IParser<A> parser, IParser<B> or)
where A : T
where B : T
{
// Convert IParser to Parser if needed
var parserA = parser as Parser<A> ?? new IParserAdapter<A>(parser);
var parserB = or as Parser<B> ?? new IParserAdapter<B>(or);
return new OneOf<A, B, T>(parserA, parserB);
}

/// <summary>
/// Builds a parser that return either of the first successful of the specified parsers.
/// </summary>
public static Parser<T> OneOf<T>(params Parser<T>[] parsers) => new OneOf<T>(parsers);

/// <summary>
/// Builds a parser that return either of the first successful of the specified parsers.
/// Uses covariance to accept parsers of derived types.
/// </summary>
public static Parser<T> OneOf<T>(params IParser<T>[] parsers)
{
// Convert IParser to Parser if needed
var converted = new Parser<T>[parsers.Length];
for (int i = 0; i < parsers.Length; i++)
{
converted[i] = parsers[i] as Parser<T> ?? new IParserAdapter<T>(parsers[i]);
}
return new OneOf<T>(converted);
}
}
8 changes: 4 additions & 4 deletions src/Samples/Json/JsonParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,23 @@ static JsonParser()

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

var json = Deferred<IJson>();

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

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

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

Json = json.Parser = jsonString.Or(jsonArray).Or(jsonObject);
Json = json.Parser = OneOf<IJson>(jsonString, jsonArray, jsonObject);
}

public static IJson Parse(string input)
Expand Down
Loading