Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Implement IParser<out T> for covariance support
Co-authored-by: sebastienros <1165805+sebastienros@users.noreply.github.com>
  • Loading branch information
Copilot and sebastienros committed Nov 9, 2025
commit daee43e16ce45a8a1c82427f1f15f8b6d8960da3
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);
}
27 changes: 27 additions & 0 deletions src/Parlot/Fluent/IParserAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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>
{
private readonly IParser<T> _parser;

public IParserAdapter(IParser<T> parser)
{
_parser = parser ?? throw new System.ArgumentNullException(nameof(parser));
}

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 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);
}
}
121 changes: 121 additions & 0 deletions test/Parlot.Tests/CovarianceTests.cs
Original file line number Diff line number Diff line change
@@ -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<Animal>(x => x) to convert each parser
// After: Can use IParser<out T> directly with covariance

// This should work due to covariance - Dog and Cat can be used where Animal is expected
IParser<Animal> animalDogParser = dogParser;
IParser<Animal> 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<Dog>(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<Cat>(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<Animal>(x => x).Or(catParser.Then<Animal>(x => x))
// After: Can use OneOf with IParser<T> directly
var animalParser = OneOf<Animal>(dogParser, catParser);

var result1 = animalParser.Parse("dog");
Assert.NotNull(result1);
Assert.IsType<Dog>(result1);
Assert.Equal("Buddy", result1.Name);
Assert.Equal("Golden Retriever", ((Dog)result1).Breed);

var result2 = animalParser.Parse("cat");
Assert.NotNull(result2);
Assert.IsType<Cat>(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<Dog, Cat, Animal>(catParser);

var result1 = animalParser.Parse("dog");
Assert.NotNull(result1);
Assert.IsType<Dog>(result1);

var result2 = animalParser.Parse("cat");
Assert.NotNull(result2);
Assert.IsType<Cat>(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<Animal>(dogParser, catParser).Compile();

var result1 = animalParser.Parse("dog");
Assert.NotNull(result1);
Assert.IsType<Dog>(result1);
Assert.Equal("Buddy", result1.Name);

var result2 = animalParser.Parse("cat");
Assert.NotNull(result2);
Assert.IsType<Cat>(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<object> 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);
}
}