Decompile C# 14 implicit span conversions (first-class span types) - #3930
Open
siegfriedpammer wants to merge 5 commits into
Open
Decompile C# 14 implicit span conversions (first-class span types)#3930siegfriedpammer wants to merge 5 commits into
siegfriedpammer wants to merge 5 commits into
Conversation
The green FirstClassSpanTypes fixture pins overload-resolution behavior the decompiler already gets right under the C# 14 implicit span conversions: calls picking the new betterness winners (ReadOnlySpan over Span/object/IEnumerable, ReadOnlySpan<string> over object[] and ReadOnlySpan<object>, MemoryExtensions.Contains over Enumerable.Contains) round-trip as plain calls, while calls picking the losing overload keep their disambiguating casts and Enumerable.Contains stays in static call form. Extension methods on span-convertible receivers, generic inference through span conversions, params betterness, and array-to-span returns are covered too. All winners were verified by executing probes compiled at LangVersion 13 vs 14. The FirstClassSpanConversions fixture is Assert.Ignore'd (#829): it specs the desired folding of compiler-emitted span-conversion helpers back into implicit conversions - MemoryExtensions.AsSpan(string), ReadOnlySpan<T>.CastUp for span variance, and covariant-array/in-arg conversions - which the decompiler currently renders as explicit helper calls or casts (recompilable and semantics-preserving, just not minimal). Both roslyn-latest configs compile the fixture and fail only at the output comparison. Assisted-by: Claude:claude-fable-5:Claude Code
The C# 14 compiler lowers implicit span conversions to calls - MemoryExtensions.AsSpan(string), ReadOnlySpan<T>.CastUp, and the span op_Implicit operators - so decompiled code showed the lowered form even though the conversion and betterness layers already implement the C# 14 rules. CallBuilder now folds those helper calls back into conversions, riding the existing mechanism: the conversion is built as an explicit cast, consumption sites make it implicit where the context allows, and the overload-resolution recheck re-adds a cast when the bare argument would bind to a different overload (which canonicalizes deliberate AsSpan disambiguations to the equivalent explicit span cast). Span conversions compose, so CastCanBeMadeImplicit lets a direct input-to-target span conversion replace a chained pair; and an rvalue bound to an in parameter gets the same chance to shed the cast as a by-value argument, since ChangeDirectionExpressionTo bypasses the by-value strip. Part of #829. Assisted-by: Claude:claude-fable-5:Claude Code
Auditing against the first-class-span-types proposal turned up three deviations, each now pinned by resolver unit tests whose expectations were established by compiling probe programs with the C# 14 compiler. Lower-bound type inference recursed into Span<T> targets as another lower-bound inference, but Span<T> is invariant and the spec demands an exact element inference there: M<T>(Span<T>, T) with (Span<string>, object) must fail inference (CS0411), not unify to T=object. Better-conversion-target compared ReadOnlySpan element types where the spec compares the span types, admitting numeric and user-defined element conversions the span types do not share: overloads taking ReadOnlySpan<int> and ReadOnlySpan<long> are ambiguous (CS0121), not resolvable. The general mutual-convertibility rule already implements the spec's span-type test, so the element-level block is simply removed; the ReadOnlySpan-over-Span identity rule stays, since it deliberately inverts that general rule. The explicit span conversion did not exist at all, and with it the rule that user-defined conversions are not considered between span-convertible types. The visible consequence: string[] to Span<object> classified as an implicit user-defined conversion via op_Implicit(object[]) plus array covariance, where the compiler reports CS0266 - only the explicit span conversion exists. Span conversions are also no longer considered for extension receivers during method group conversion (CS0123), while invocations keep them. Part of #829. Assisted-by: Claude:claude-fable-5:Claude Code
Compiling probes with the C# 14 compiler establishes the matrix: a value argument binds to an 'in ReadOnlySpan<T>' parameter with and without the span conversion (a temporary is created), while an explicit 'in' argument demands the parameter's own type (CS1503); and for a by-value/'in' overload pair, a call without 'in' picks the by-value overload - also through the span conversion - while 'in' at the call site makes the in-overload the only candidate (CS1615 with a conversion). The fixture pins the decompiler side of the same matrix: 'in' must survive decompilation where it disambiguates the overload pair, and the folded span-conversion argument must re-resolve to the by-value winner. The resolver unit tests pin applicability and betterness directly. All of these were green as written - they fence the implicit-in cast stripping and the recheck ladder against regressions rather than fixing a defect. Part of #829. Assisted-by: Claude:claude-fable-5:Claude Code
The remaining parameter-modifier dimension of the C# 14 rules: an expanded params call prefers the params ReadOnlySpan overload over params array (the C# 13 better-params-collection rule) while the normal form keeps the exact array overload; and a span conversion never binds a ref or out parameter, so a keyword-less argument picks the by-value overload and the ref/out keyword must survive decompilation to keep binding the by-ref one. All expectations come from compiling probes (the negative directions are CS1503) and everything was green as written - these are lock-downs, not fixes. Part of #829. Assisted-by: Claude:claude-fable-5:Claude Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #829 (does not close it).
Implements decompilation of C# 14 implicit span conversions (first-class span types) and aligns the resolver with that spec. This PR started as a tests-only PR: the
FirstClassSpanConversionsfixture landed as anAssert.Ignore'd desired-output spec. The implementation followed on the same branch; the ignore is gone and that fixture is now the acceptance test.What the decompiler now does
The C# 14 compiler lowers implicit span conversions to helper calls -
MemoryExtensions.AsSpan(string),ReadOnlySpan<T>.CastUp<TDerived>, and the spanop_Implicitoperators - so decompiled code showed the lowered form:s.AsSpan(),ReadOnlySpan<Base>.CastUp(s),(ReadOnlySpan<int>)a,(Base[]?)a.CallBuildernow folds these back into conversions, riding the existing conversion machinery:ConversionResolveResult;inarguments) make the cast implicit where the context allows;str.AsSpan()overload disambiguation therefore canonicalizes to the equivalent explicit span cast(ReadOnlySpan<char>)str.Gated by the existing
DecompilerSettings.FirstClassSpanTypes(C# 14.0), so older language targets keep the helper calls.Resolver alignment with the spec
Auditing the pre-existing span groundwork against the proposal found three deviations. Each is fixed and pinned by unit tests whose expectations were established by compiling probe programs with the C# 14 compiler:
Span<T>target now makes an exact element inference (Span<T>is invariant) -M<T>(Span<T>, T)called with(Span<string>, object)fails inference (CS0411) instead of unifying toT = object.ReadOnlySpan<E1>vsReadOnlySpan<E2>is decided by implicit convertibility between the span types, not the element types - overloads onReadOnlySpan<int>/ReadOnlySpan<long>are ambiguous (CS0121), not resolvable.Span<object> s = stringArray;is CS0266, not an implicit conversion viaop_Implicit(object[])plus array covariance. Span conversions are also excluded from extension receivers during method-group conversion (CS0123), while invocations keep them.Tests
FirstClassSpanTypes(pretty, green from the start): pins the C# 14 overload-resolution winners under span betterness.FirstClassSpanConversions(pretty, the acceptance test): every conversion shape - string toReadOnlySpan<char>in argument, return, local-initializer and extension-receiver position; span variance viaCastUp; covariant arrays; implicit-inarguments; and a by-value/inoverload pair where theinkeyword must survive decompilation to keep binding the same overload.ICSharpCode.Decompiler.Tests/Semantics/(inference, betterness, conversion classification, method-group receivers, and thein-binding matrix), each annotated with the Roslyn diagnostic it mirrors.Full decompiler suite on Linux: 3099 passed / 0 failed / 44 skipped, and a clean
-t:RebuildofILSpy.XPlat.slnf.This PR was prepared by an AI agent (Claude) on behalf of @siegfriedpammer.