Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 9, 2025

Introduce IParser<out T> for covariance support - COMPLETED

Summary

This PR introduces IParser<out T> interface to enable covariance, eliminating the need for wasteful .Then<TBase>(x => x) conversions when using parsers with OneOf.

Checklist

  • Introduce IParser<out T> interface with covariant type parameter
  • Design Parse method that works with covariance (cannot use ref ParseResult<T>)
  • Make Parser<T> implement IParser<T>
  • Update OneOf to accept IParser<T> for better type flexibility
  • Add tests to verify covariance works as expected
  • Ensure all existing tests pass (712/712 on net9.0, 684/684 on net8.0)
  • Add documentation and examples
  • Run security scanner (0 vulnerabilities found)
  • Simplify existing samples (JSON, SQL) using covariance
  • Make IParserAdapter implement ISeekable for optimization
  • Make IParserAdapter implement ICompilable for compilation support

Implementation Details

Files Added

  • src/Parlot/Fluent/IParser.cs - New covariant interface with out T variance
  • src/Parlot/Fluent/IParserAdapter.cs - Internal adapter to convert IParser to Parser, implements ISeekable and ICompilable
  • test/Parlot.Tests/CovarianceTests.cs - 5 comprehensive tests covering various scenarios including compilation
  • docs/covariance-example.md - Before/after comparison documentation
  • docs/covariance-usage.cs - Practical usage example with expression parser

Files Modified

  • src/Parlot/Fluent/Parser.cs - Now implements IParser with explicit interface implementation
  • src/Parlot/Fluent/Parsers.OneOf.cs - Added overloads accepting IParser[] parameters
  • src/Samples/Json/JsonParser.cs - Simplified using covariance (removed 3 type annotations, replaced .Or() chain)
  • src/Samples/Sql/SqlParser.cs - Simplified using covariance (removed 20+ type annotations, eliminated 2 identity conversions)

Latest Changes

  • IParserAdapter now implements ISeekable: Forwards CanSeek, ExpectedChars, and SkipWhitespace properties from the wrapped parser when it implements ISeekable. This preserves optimization opportunities when using covariant parsers.
  • IParserAdapter now implements ICompilable: When the wrapped parser is a Parser<T>, delegates compilation to it via Build(). Otherwise, provides a fallback implementation that uses the Parse method. This ensures covariant parsers can be compiled for better performance.

Technical Solution

The challenge was that ref ParseResult<T> doesn't support covariance due to C# language constraints. The solution:

  1. IParser<out T>.Parse(context, out int start, out int end, out object? value) - Returns value as object to enable covariance
  2. Parser<T> explicitly implements this interface while keeping the original Parse(context, ref ParseResult<T>) method for backward compatibility
  3. New OneOf overloads accept IParser<T> parameters, enabling direct covariant usage
  4. IParserAdapter<T> transparently adapts IParser to Parser, implementing both ISeekable and ICompilable to preserve optimization hints and compilation support

Sample Improvements

JSON Parser

  • Removed 3 explicit .Then<IJson> type conversions
  • Replaced .Or() chain with OneOf<IJson>()
  • Cleaner, more declarative syntax

SQL Parser

  • Removed 20+ explicit .Then<Expression> type annotations
  • Eliminated 2 identity conversions (.Then<TableSource>(t => t)) - the exact wasteful pattern mentioned in the issue!
  • Replaced 5 .Or() chains with OneOf<T>()
  • Significantly improved readability

Benefits

  • No wrapper objects: Reuses original parser instances without creating Then wrappers
  • Cleaner syntax: More readable code without explicit type conversions
  • Better performance: Eliminates overhead of wrapper parser objects
  • Preserves optimizations: IParserAdapter forwards ISeekable properties for lookup table optimizations
  • Compilation support: IParserAdapter implements ICompilable, enabling covariant parsers to be compiled
  • Type safety: Maintains full type safety through covariant interface
  • Backward compatible: All existing code continues to work without changes

Test Results

✅ All 712 tests pass on .NET 9.0
✅ All 684 tests pass on .NET 8.0
✅ 0 security vulnerabilities found
✅ 5 new covariance-specific tests added (including compilation test)
✅ All existing samples simplified and still work correctly

Example Usage

Before:

var dogParser = Terms.Text("dog").Then(_ => new Dog());
var catParser = Terms.Text("cat").Then(_ => new Cat());
var animalParser = dogParser.Then<Animal>(x => x).Or(catParser.Then<Animal>(x => x));

After:

var dogParser = Terms.Text("dog").Then(_ => new Dog());
var catParser = Terms.Text("cat").Then(_ => new Cat());
var animalParser = OneOf<Animal>(dogParser, catParser); // Clean and simple!
// Can also be compiled for better performance:
var compiledParser = animalParser.Compile();
Original prompt

This section details on the original issue you should resolve

<issue_title>Introduce IParser<out T></issue_title>
<issue_description>Right now when you want to use a Parser<T> as Parser<TBase> (in OneOf, for example) you'd need to add .Then<TBase>(x => x) which is wasteful. By operating on a covariant IParser<T> the instance could be reused for both without creating a new one.</issue_description>

<agent_instructions>I am not sure there is a solution to this, but please try to find one.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@sebastienros How would this be changed? `ParseResult` is invalid here.
public interface IParser<out T>
{
    bool Parse(ParseContext context, ref ParseResult<T> result);

#if NETCOREAPP
    public IParser<U?> Then<U>() => new Then<T, U?>(this, default(U));
#endif
}

public abstract partial class Parser<T> : IParser<T>
...
```</body></comment_new>
</comments>

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Introduce IParser<out T> for better reuse Introduce IParser<out T> for covariance support Nov 9, 2025
Copilot AI requested a review from sebastienros November 9, 2025 17:06
Copilot finished work on behalf of sebastienros November 9, 2025 17:06
@sebastienros sebastienros marked this pull request as ready for review November 9, 2025 17:14
@sebastienros
Copy link
Owner

@copilot can the existing samples (json, calc, sql) be simplified with covariance?

Copy link
Contributor Author

Copilot AI commented Nov 10, 2025

@copilot can the existing samples (json, calc, sql) be simplified with covariance?

Yes! The samples have been simplified using covariance in commit a3d82bf.

JSON Parser:

  • Removed 3 .Then<IJson> type annotations
  • Replaced .Or() chain with OneOf<IJson>()

SQL Parser:

  • Removed 20+ .Then<Expression> type annotations
  • Eliminated 2 identity conversions including .Then<TableSource>(t => t) - the exact wasteful pattern from the issue
  • Replaced 5 .Or() chains with OneOf<T>()

All 712 tests pass. The code is now cleaner and more declarative.

Copilot finished work on behalf of sebastienros November 10, 2025 10:02
.Or(parameter);
var identifierExpr = identifier.Then(id => new IdentifierExpression(id));

var term = OneOf<Expression>(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot is it possible to write this Parser<Expression> term = functionCall.Or(parSelectStatement, ...) in order to keep using Or()? Or something similar where <Expression> would still need to be called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it's technically possible to add a variadic .Or<T>() method that accepts multiple parsers with an explicit type parameter (like functionCall.Or<Expression>(parSelectStatement, ...)), I've reverted the changes to keep using OneOf<Expression>(...) as originally implemented.

The OneOf<T>() syntax remains the recommended approach for combining multiple covariant parsers, as it's clearer and more consistent with the existing API design.

@sebastienros
Copy link
Owner

@copilot Should IParserAdapter implement ISeekable by the way of the wrapped instance?

@sebastienros
Copy link
Owner

@copilot Should IParserAdapter implement ICompilable?

Copy link
Contributor Author

Copilot AI commented Nov 10, 2025

@copilot Should IParserAdapter implement ISeekable by the way of the wrapped instance?

Yes, good catch! IParserAdapter now implements ISeekable and forwards the CanSeek, ExpectedChars, and SkipWhitespace properties from the wrapped parser (when it implements ISeekable). This follows the same pattern as Then<T, U> and other wrapper parsers, preserving optimization opportunities for lookup tables.

Fixed in commit 8df2005.

Copy link
Contributor Author

Copilot AI commented Nov 10, 2025

@copilot Should IParserAdapter implement ICompilable?

Yes! IParserAdapter now implements ICompilable. When the wrapped parser is a Parser<T>, it delegates compilation to it via Build(). Otherwise, it provides a fallback implementation using the Parse method. This ensures covariant parsers can be compiled for better performance.

The existing test CovariantParsersShouldWorkWithCompilation already validates this works correctly.

Fixed in commit b323754.

@sebastienros sebastienros enabled auto-merge (squash) November 10, 2025 10:39
Copilot finished work on behalf of sebastienros November 10, 2025 10:39
@sebastienros sebastienros merged commit d9f41b5 into main Nov 10, 2025
2 checks passed
@sebastienros sebastienros deleted the copilot/introduce-iparser-out-t branch November 10, 2025 10:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Introduce IParser<out T>

2 participants