diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
index e3fd7f88e..028dbba14 100644
--- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
+++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
@@ -208,6 +208,8 @@
+
+
diff --git a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
index c1c6070b8..85615a133 100644
--- a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
+++ b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
@@ -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()
{
diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
index 82d29611d..3a16f4270 100644
--- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
+++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
@@ -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);
}
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SpanConversionOperatorMismatch.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SpanConversionOperatorMismatch.cs
new file mode 100644
index 000000000..c2dfcf0d4
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SpanConversionOperatorMismatch.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace ICSharpCode.Decompiler.Tests.TestCases.ILPretty
+{
+ public class SpanConversionOperatorMismatch
+ {
+ public static implicit operator ReadOnlySpan(object o)
+ {
+ return default(ReadOnlySpan);
+ }
+
+ public static ReadOnlySpan ConvertString(string s)
+ {
+ return (ReadOnlySpan)(object)s;
+ }
+ }
+}
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SpanConversionOperatorMismatch.il b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SpanConversionOperatorMismatch.il
new file mode 100644
index 000000000..f26f3c531
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SpanConversionOperatorMismatch.il
@@ -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(object)' - neither operand type is the
+// declaring type - but IL can. An implicit span conversion from string to ReadOnlySpan 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
+ op_Implicit(object o) cil managed
+ {
+ .maxstack 1
+ .locals init (valuetype [System.Runtime]System.ReadOnlySpan`1 V_0)
+ IL_0000: ldloca.s V_0
+ IL_0002: initobj valuetype [System.Runtime]System.ReadOnlySpan`1
+ IL_0008: ldloc.0
+ IL_0009: ret
+ }
+
+ .method public hidebysig static
+ valuetype [System.Runtime]System.ReadOnlySpan`1
+ ConvertString(string s) cil managed
+ {
+ .maxstack 8
+ IL_0000: ldarg.0
+ IL_0001: call valuetype [System.Runtime]System.ReadOnlySpan`1 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
+ }
+}
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanConversions.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanConversions.cs
index 8888496bd..4171ce9ed 100644
--- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanConversions.cs
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanConversions.cs
@@ -30,6 +30,18 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
}
+ internal class SpanConvertible
+ {
+ public static implicit operator ReadOnlySpan(SpanConvertible c)
+ {
+ return default(ReadOnlySpan);
+ }
+ }
+
+ internal class DerivedSpanConvertible : SpanConvertible
+ {
+ }
+
public static void AcceptReadOnlySpanChar(ReadOnlySpan s)
{
}
@@ -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 readOnlySpan = s;
- return readOnlySpan.Length;
+ return readOnlySpan.Length + readOnlySpan.Length;
}
public static void VarianceReadOnlySpan(ReadOnlySpan s)
@@ -88,6 +102,23 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
AcceptReadOnlySpanBase(a);
}
+ public static ReadOnlySpan CovariantArrayThroughOperator(Derived[] a)
+ {
+ // ReadOnlySpan.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 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);
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanTypes.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanTypes.cs
index 82058e290..3b01982ca 100644
--- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanTypes.cs
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanTypes.cs
@@ -173,7 +173,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
ObjectOrReadOnlySpan((object)arr);
ObjectOrReadOnlySpanChar((object)str);
EnumerableOrReadOnlySpan((IEnumerable)arr);
- StringOrReadOnlySpanChar(str.AsSpan());
+ StringOrReadOnlySpanChar((ReadOnlySpan)str);
}
}
diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs
index 62e7d9f66..0bfb63143 100644
--- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs
+++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs
@@ -329,6 +329,30 @@ namespace ICSharpCode.Decompiler.CSharp
&& method.Parameters[0].Type.IsKnownType(KnownTypeCode.String);
}
+ ///
+ /// Matches MemoryExtensions.AsSpan(string), the helper the C# 14 compiler emits for the
+ /// implicit span conversion from string to ReadOnlySpan<char>.
+ ///
+ 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);
+ }
+
+ ///
+ /// Matches ReadOnlySpan<To>.CastUp<From>(ReadOnlySpan<From>), the helper the
+ /// C# 14 compiler emits for the covariant implicit span conversion.
+ ///
+ 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 callArguments,
IReadOnlyList? argumentToParameterMap = null,
@@ -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: "", Name: "InlineArrayAsSpan" or "InlineArrayAsReadOnlySpan" }
&& argumentList.Length == 2)
@@ -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
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);
diff --git a/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs b/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs
index efbf1ee69..1a9ca1a85 100644
--- a/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs
+++ b/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs
@@ -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);
}