Skip to content

Decompile C# 14 implicit span conversions (first-class span types) - #3930

Open
siegfriedpammer wants to merge 5 commits into
masterfrom
tests/829-first-class-span
Open

Decompile C# 14 implicit span conversions (first-class span types)#3930
siegfriedpammer wants to merge 5 commits into
masterfrom
tests/829-first-class-span

Conversation

@siegfriedpammer

@siegfriedpammer siegfriedpammer commented Jul 29, 2026

Copy link
Copy Markdown
Member

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 FirstClassSpanConversions fixture landed as an Assert.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 span op_Implicit operators - so decompiled code showed the lowered form: s.AsSpan(), ReadOnlySpan<Base>.CastUp(s), (ReadOnlySpan<int>)a, (Base[]?)a. CallBuilder now folds these back into conversions, riding the existing conversion machinery:

  • the fold always builds an explicit cast carrying a ConversionResolveResult;
  • consumption sites (arguments, returns, initializers, extension receivers, implicit-in arguments) make the cast implicit where the context allows;
  • the overload-resolution recheck re-adds a cast when the bare argument would bind to a different overload. A deliberate 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:

  • Type inference: lower-bound inference into a 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 to T = object.
  • Betterness: ReadOnlySpan<E1> vs ReadOnlySpan<E2> is decided by implicit convertibility between the span types, not the element types - overloads on ReadOnlySpan<int>/ReadOnlySpan<long> are ambiguous (CS0121), not resolvable.
  • Explicit span conversion: newly modeled, together with the spec rule that user-defined conversions are not considered between span-convertible types - Span<object> s = stringArray; is CS0266, not an implicit conversion via op_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 to ReadOnlySpan<char> in argument, return, local-initializer and extension-receiver position; span variance via CastUp; covariant arrays; implicit-in arguments; and a by-value/in overload pair where the in keyword must survive decompilation to keep binding the same overload.
  • 17 resolver unit tests in ICSharpCode.Decompiler.Tests/Semantics/ (inference, betterness, conversion classification, method-group receivers, and the in-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:Rebuild of ILSpy.XPlat.slnf.

This PR was prepared by an AI agent (Claude) on behalf of @siegfriedpammer.

@siegfriedpammer siegfriedpammer changed the title Add pretty tests for C# 14 first-class span types Decompile C# 14 implicit span conversions (first-class span types) Jul 30, 2026
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
@siegfriedpammer
siegfriedpammer force-pushed the tests/829-first-class-span branch from d47f524 to bf93ef3 Compare July 31, 2026 11:57
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.

1 participant