Browse Source

Fold C# 14 implicit span conversions instead of printing their helpers

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
pull/3930/head
Siegfried Pammer 2 months ago committed by Christoph Wille
parent
commit
cd181b3f08
  1. 2
      ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
  2. 6
      ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
  3. 1
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  4. 17
      ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SpanConversionOperatorMismatch.cs
  5. 57
      ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SpanConversionOperatorMismatch.il
  6. 33
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanConversions.cs
  7. 2
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanTypes.cs
  8. 61
      ICSharpCode.Decompiler/CSharp/CallBuilder.cs
  9. 7
      ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs

2
ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj

@ -208,6 +208,8 @@ @@ -208,6 +208,8 @@
<Compile Remove="TestCases\ILPretty\Issue3729.cs" />
<None Include="TestCases\ILPretty\Issue3729.cs" />
<None Include="TestCases\ILPretty\Issue3729.il" />
<Compile Remove="TestCases\ILPretty\SpanConversionOperatorMismatch.cs" />
<None Include="TestCases\ILPretty\SpanConversionOperatorMismatch.cs" />
<Compile Remove="TestCases\ILPretty\FSharpLoops_Debug.cs" />
<None Include="TestCases\ILPretty\FSharpLoops_Debug.cs" />
<Compile Remove="TestCases\ILPretty\FSharpLoops_Release.cs" />

6
ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs

@ -303,6 +303,12 @@ namespace ICSharpCode.Decompiler.Tests @@ -303,6 +303,12 @@ namespace ICSharpCode.Decompiler.Tests
await Run();
}
[Test]
public async Task SpanConversionOperatorMismatch()
{
await Run(settings: new DecompilerSettings { FileScopedNamespaces = false, FirstClassSpanTypes = true });
}
[Test]
public async Task ConstantBlobs()
{

1
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

@ -809,7 +809,6 @@ namespace ICSharpCode.Decompiler.Tests @@ -809,7 +809,6 @@ namespace ICSharpCode.Decompiler.Tests
[Test]
public async Task FirstClassSpanConversions([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
{
Assert.Ignore("Implicit span conversions (C# 14 first-class span types) are not yet folded by the decompiler. See https://github.com/icsharpcode/ILSpy/issues/829");
await RunForLibrary(cscOptions: cscOptions);
}

17
ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SpanConversionOperatorMismatch.cs

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
using System;
namespace ICSharpCode.Decompiler.Tests.TestCases.ILPretty
{
public class SpanConversionOperatorMismatch
{
public static implicit operator ReadOnlySpan<char>(object o)
{
return default(ReadOnlySpan<char>);
}
public static ReadOnlySpan<char> ConvertString(string s)
{
return (ReadOnlySpan<char>)(object)s;
}
}
}

57
ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SpanConversionOperatorMismatch.il

@ -0,0 +1,57 @@ @@ -0,0 +1,57 @@
// Regression fixture: an implicit span conversion must not be folded into a call to a conversion
// operator that does not perform it.
//
// C# cannot declare 'implicit operator ReadOnlySpan<char>(object)' - neither operand type is the
// declaring type - but IL can. An implicit span conversion from string to ReadOnlySpan<char> does
// exist, yet the compiler emits it as MemoryExtensions.AsSpan, not as this operator, so the call
// below is a user-defined conversion and the cast to its parameter type has to survive.
.assembly extern System.Runtime
{
.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )
.ver 4:0:0:0
}
.assembly SpanConversionOperatorMismatch
{
.custom instance void [System.Runtime]System.Runtime.Versioning.TargetFrameworkAttribute::.ctor(string) = {
string('.NETCoreApp,Version=11.0')
}
.hash algorithm 0x00008004
.ver 1:0:0:0
}
.module SpanConversionOperatorMismatch.dll
.class public auto ansi beforefieldinit ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpanConversionOperatorMismatch
extends [System.Runtime]System.Object
{
.method public hidebysig specialname static
valuetype [System.Runtime]System.ReadOnlySpan`1<char>
op_Implicit(object o) cil managed
{
.maxstack 1
.locals init (valuetype [System.Runtime]System.ReadOnlySpan`1<char> V_0)
IL_0000: ldloca.s V_0
IL_0002: initobj valuetype [System.Runtime]System.ReadOnlySpan`1<char>
IL_0008: ldloc.0
IL_0009: ret
}
.method public hidebysig static
valuetype [System.Runtime]System.ReadOnlySpan`1<char>
ConvertString(string s) cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: call valuetype [System.Runtime]System.ReadOnlySpan`1<char> ICSharpCode.Decompiler.Tests.TestCases.ILPretty.SpanConversionOperatorMismatch::op_Implicit(object)
IL_0006: ret
}
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [System.Runtime]System.Object::.ctor()
IL_0006: ret
}
}

33
ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanConversions.cs

@ -30,6 +30,18 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -30,6 +30,18 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
}
internal class SpanConvertible
{
public static implicit operator ReadOnlySpan<int>(SpanConvertible c)
{
return default(ReadOnlySpan<int>);
}
}
internal class DerivedSpanConvertible : SpanConvertible
{
}
public static void AcceptReadOnlySpanChar(ReadOnlySpan<char> s)
{
}
@ -64,8 +76,10 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -64,8 +76,10 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
public static int StringToReadOnlySpanCharLocal(string s)
{
// The local is read twice so it survives decompilation; a single-use span local is
// inlined into its consumer by general decompiler policy, independent of this feature.
ReadOnlySpan<char> readOnlySpan = s;
return readOnlySpan.Length;
return readOnlySpan.Length + readOnlySpan.Length;
}
public static void VarianceReadOnlySpan(ReadOnlySpan<Derived> s)
@ -88,6 +102,23 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -88,6 +102,23 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
AcceptReadOnlySpanBase(a);
}
public static ReadOnlySpan<Base> CovariantArrayThroughOperator(Derived[] a)
{
// ReadOnlySpan<Base>.op_Implicit(Base[]) applied to a Derived[]: array covariance
// means the IL passes the argument without a cast instruction, and the span
// conversion the call performs relates exactly these two types.
return a;
}
public static ReadOnlySpan<int> UserDefinedOperatorToSpan(DerivedSpanConvertible c)
{
// The operator is declared on the base type, so the conversion is user-defined even
// though its target is a span type. Only the user-defined conversion that resolves
// to this very operator may be folded into the cast; a span conversion never applies
// to a source type outside the language's own span-convertible set.
return c;
}
public static void InArgument(int[] a)
{
AcceptInReadOnlySpan(a);

2
ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanTypes.cs

@ -173,7 +173,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -173,7 +173,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
ObjectOrReadOnlySpan((object)arr);
ObjectOrReadOnlySpanChar((object)str);
EnumerableOrReadOnlySpan((IEnumerable<int>)arr);
StringOrReadOnlySpanChar(str.AsSpan());
StringOrReadOnlySpanChar((ReadOnlySpan<char>)str);
}
}

61
ICSharpCode.Decompiler/CSharp/CallBuilder.cs

@ -329,6 +329,30 @@ namespace ICSharpCode.Decompiler.CSharp @@ -329,6 +329,30 @@ namespace ICSharpCode.Decompiler.CSharp
&& method.Parameters[0].Type.IsKnownType(KnownTypeCode.String);
}
/// <summary>
/// Matches MemoryExtensions.AsSpan(string), the helper the C# 14 compiler emits for the
/// implicit span conversion from string to ReadOnlySpan&lt;char&gt;.
/// </summary>
internal static bool IsStringToReadOnlySpanCharAsSpan(IMethod method)
{
return method is { IsStatic: true, Name: "AsSpan", Parameters.Count: 1, TypeArguments.Count: 0 }
&& method.DeclaringType.FullName == "System.MemoryExtensions"
&& method.ReturnType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)
&& method.ReturnType.TypeArguments[0].IsKnownType(KnownTypeCode.Char)
&& method.Parameters[0].Type.IsKnownType(KnownTypeCode.String);
}
/// <summary>
/// Matches ReadOnlySpan&lt;To&gt;.CastUp&lt;From&gt;(ReadOnlySpan&lt;From&gt;), the helper the
/// C# 14 compiler emits for the covariant implicit span conversion.
/// </summary>
internal static bool IsReadOnlySpanCastUp(IMethod method)
{
return method is { IsStatic: true, Name: "CastUp", Parameters.Count: 1, TypeArguments.Count: 1 }
&& method.DeclaringType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)
&& method.Parameters[0].Type.IsKnownType(KnownTypeCode.ReadOnlySpanOfT);
}
public ExpressionWithResolveResult Build(OpCode callOpCode, IMethod method,
IReadOnlyList<ILInstruction> callArguments,
IReadOnlyList<int>? argumentToParameterMap = null,
@ -481,6 +505,21 @@ namespace ICSharpCode.Decompiler.CSharp @@ -481,6 +505,21 @@ namespace ICSharpCode.Decompiler.CSharp
return HandleImplicitConversion(method, argumentList.Arguments[0]);
}
if (settings.FirstClassSpanTypes && argumentList.Length == 1
&& (IsStringToReadOnlySpanCharAsSpan(method) || IsReadOnlySpanCastUp(method)))
{
// The C# 14 compiler emits these helpers for implicit span conversions; fold the
// call back into the conversion. Only safe when the conversion actually applies
// to this argument type - otherwise keep the call (e.g. AsSpan on a null literal).
var spanConv = CSharpConversions.Get(expressionBuilder.compilation)
.ImplicitConversion(argumentList.Arguments[0].Type, method.ReturnType);
if (spanConv.IsImplicitSpanConversion)
{
argumentList.CheckNoNamedOrOptionalArguments();
return HandleImplicitConversion(method, argumentList.Arguments[0]);
}
}
if (settings.InlineArrays
&& method is { DeclaringType.FullName: "<PrivateImplementationDetails>", Name: "InlineArrayAsSpan" or "InlineArrayAsReadOnlySpan" }
&& argumentList.Length == 2)
@ -1024,6 +1063,15 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1024,6 +1063,15 @@ namespace ICSharpCode.Decompiler.CSharp
if (parameter.ReferenceKind != ReferenceKind.None)
{
arg = ExpressionBuilder.ChangeDirectionExpressionTo(arg, parameter.ReferenceKind, callArguments[i] is AddressOf);
// An rvalue bound to an 'in' parameter loses its DirectionExpression above and
// is an ordinary value expression: give a span conversion the same chance to
// become implicit that by-value arguments get from the ConvertTo call above.
if (arg.Expression is not DirectionExpression
&& parameter.Type.SkipModifiers() is ByReferenceType brt
&& arg.ResolveResult is ConversionResolveResult { Conversion.IsImplicitSpanConversion: true })
{
arg = arg.ConvertTo(brt.ElementType, expressionBuilder, allowImplicitConversion: true);
}
}
arguments.Add(arg);
@ -1535,7 +1583,18 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1535,7 +1583,18 @@ namespace ICSharpCode.Decompiler.CSharp
var conversions = CSharpConversions.Get(expressionBuilder.compilation);
IType targetType = method.ReturnType;
var conv = conversions.ImplicitConversion(argument.Type, targetType);
if (!(conv.IsUserDefined && conv.IsValid && conv.Method.Equals(method, NormalizeTypeVisitor.TypeErasure)))
// The compiler emits an implicit span conversion as a call to one of the span types'
// own members, so such a call is the conversion and folding it back is exact. Any
// other method reaching this point is a user-defined conversion operator, which only
// the user-defined conversion resolving to that very operator may be folded into.
bool spanConversionMember = method.DeclaringType.IsKnownType(KnownTypeCode.SpanOfT)
|| method.DeclaringType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)
|| IsStringToReadOnlySpanCharAsSpan(method);
bool directlyConvertible = conv.IsValid
&& (conv.IsUserDefined
? conv.Method.Equals(method, NormalizeTypeVisitor.TypeErasure)
: conv.IsImplicitSpanConversion && spanConversionMember);
if (!directlyConvertible)
{
// implicit conversion to targetType isn't directly possible, so first insert a cast to the argument type
argument = argument.ConvertTo(method.Parameters[0].Type, expressionBuilder);

7
ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs

@ -652,6 +652,13 @@ namespace ICSharpCode.Decompiler.CSharp @@ -652,6 +652,13 @@ namespace ICSharpCode.Decompiler.CSharp
return newTargetType.IsKnownType(KnownTypeCode.FormattableString)
|| newTargetType.IsKnownType(KnownTypeCode.IFormattable);
}
if (conversion.IsImplicitSpanConversion)
{
// Implicit span conversions compose: if the input converts to the new target
// directly, the result is the same span the two-step path produces.
return conversions.IdentityConversion(oldTargetType, newTargetType)
|| conversions.ImplicitConversion(inputType, newTargetType).IsImplicitSpanConversion;
}
return conversions.IdentityConversion(oldTargetType, newTargetType);
}

Loading…
Cancel
Save