Skip to content

Generic sweeps call the span overload when they mean Parse(string): ISpanParsable hides IParsable in overload resolution #142

Closed

Description

@CaffeinatedCoder

Found while writing the sibling parity sweeps (#139).

Problem

In generic code constrained on IRangeFactory<TRange, T> or IValueSetFactory<TSet, T>, a call such as TSet.Parse(text, provider) or TSet.TryParse(text, provider, out var r) with a string argument binds to the span overload. Both interfaces derive from ISpanParsable<TSelf>. Overload resolution removes the base interface's IParsable<TSelf>.Parse(string, …) candidate whenever the derived interface's Parse(ReadOnlySpan<char>, …) is applicable, and the implicit string → ReadOnlySpan<char> conversion makes it applicable.

Measured: with Int32Set.TryParse(string) seeded to read a blank literal as {}, a generic TSet.TryParse(" ", …) under an IValueSetFactory constraint still answers false. The same call under an IParsable<TSet> constraint answers true.

So the generic convention sweeps that say they cover Parse/TryParse "through every entry point" never call the string overloads. Each type declares those overloads separately (SetFormat.Parse(s.AsSpan(), …)), and they're what reflection-based consumers find, such as minimal-API binding's TryParse lookup. Today every pair delegates to the same parser, so nothing is wrong yet. But a string overload can drift without any sweep noticing.

Call sites (generic string calls under such a constraint):

  • DecorativeElementContractTests
  • LiteralProviderContractTests (3)
  • RangeLiteralGrammarContractTests (3)
  • SetElementWhiteSpaceContractTests (4)
  • TemporalLiteralStrictnessContractTests (8)
  • WrapperGrammarContractTests (3)
  • ShapeMatrixParityTests (integration, 1)

Change

Route those calls through StringOverloads.Parse<T> / StringOverloads.TryParse<T> (added in #139, constrained on IParsable<TSelf> alone). Where a sweep lists Parse(span) as a separate entry point, it then really compares the two. For the oracles among them, show the reach with the set-tryparse-reads-blank-as-empty seed or a string-only sibling.

Done

  • No generic Parse/TryParse call with a string argument goes through an ISpanParsable-derived constraint (a convention test could hold this, in the style of EngineDispatchConventionTests)
  • A seed that changes only a string overload is caught by the literal sweeps that claim it

Activity

  1. added
    testsAdds or changes tests only (oracles, harnesses); no library code
    package/nodatimeAffects CodoMetis.ValueRanges.NodaTime
    area/literalsParse, TryParse and formatting
    package/coreAffects CodoMetis.ValueRanges
    on Oct 18, 2026
  2. CaffeinatedCoder commented on Oct 18, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Implemented in #144, stacked on #143. The scope grew by two classes that landed on main after this was filed: CultureEntryPointProbeTests (constrained on ISpanParsable<TValue> directly, which has the same trap) and the integration LiteralCorpusProbeTests. That makes 33 calls in ten classes. The guard is StringOverloadConventionTests, which binds the calls with Roslyn and is proven by putting one back. The reach is shown by the seed string-parse-localizes-decimal-separator: LiteralProviderContractTests and CultureEntryPointProbeTests missed it before and catch it now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/literalsParse, TryParse and formattingpackage/coreAffects CodoMetis.ValueRangespackage/nodatimeAffects CodoMetis.ValueRanges.NodaTimetestsAdds or changes tests only (oracles, harnesses); no library code

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions