Repository navigation
Description
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):
DecorativeElementContractTestsLiteralProviderContractTests(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/TryParsecall with astringargument goes through anISpanParsable-derived constraint (a convention test could hold this, in the style ofEngineDispatchConventionTests) - A seed that changes only a string overload is caught by the literal sweeps that claim it
Activity
CaffeinatedCoder commentedon Oct 18, 2026
Implemented in #144, stacked on #143. The scope grew by two classes that landed on
mainafter this was filed:CultureEntryPointProbeTests(constrained onISpanParsable<TValue>directly, which has the same trap) and the integrationLiteralCorpusProbeTests. That makes 33 calls in ten classes. The guard isStringOverloadConventionTests, which binds the calls with Roslyn and is proven by putting one back. The reach is shown by the seedstring-parse-localizes-decimal-separator:LiteralProviderContractTestsandCultureEntryPointProbeTestsmissed it before and catch it now.