Browse Source

Merge pull request #3930 from icsharpcode/tests/829-first-class-span

Decompile C# 14 implicit span conversions (first-class span types)
pull/3897/head
Siegfried Pammer 1 month ago committed by GitHub
parent
commit
0447e2b58f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      .gitattributes
  2. 2
      ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
  3. 6
      ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
  4. 12
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  5. 69
      ICSharpCode.Decompiler.Tests/Semantics/ConversionTests.cs
  6. 38
      ICSharpCode.Decompiler.Tests/Semantics/ExplicitConversionTest.cs
  7. 175
      ICSharpCode.Decompiler.Tests/Semantics/OverloadResolutionTests.cs
  8. 48
      ICSharpCode.Decompiler.Tests/Semantics/RefAssemblyCompilation.cs
  9. 123
      ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs
  10. 17
      ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SpanConversionOperatorMismatch.cs
  11. 57
      ICSharpCode.Decompiler.Tests/TestCases/ILPretty/SpanConversionOperatorMismatch.il
  12. 152
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanConversions.cs
  13. 216
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanTypes.cs
  14. 12
      ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs
  15. 74
      ICSharpCode.Decompiler/CSharp/CallBuilder.cs
  16. 87
      ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs
  17. 12
      ICSharpCode.Decompiler/CSharp/Resolver/OverloadResolution.cs
  18. 6
      ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs
  19. 7
      ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs
  20. 17
      ICSharpCode.Decompiler/Semantics/Conversion.cs
  21. 4
      ICSharpCode.Decompiler/Semantics/MethodGroupResolveResult.cs

1
.gitattributes vendored

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
*.cs text eol=crlf diff=csharp
*.sln text eol=crlf
*.csproj text eol=crlf
*.resx text eol=crlf
# Consumed verbatim by Linux tools (shell-executed rpm spec, dpkg control, desktop entry);
# CRLF breaks them, so keep them LF even in Windows working trees.
*.spec text eol=lf

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

@ -212,6 +212,8 @@ @@ -212,6 +212,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()
{

12
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

@ -800,6 +800,18 @@ namespace ICSharpCode.Decompiler.Tests @@ -800,6 +800,18 @@ namespace ICSharpCode.Decompiler.Tests
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task FirstClassSpanTypes([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task FirstClassSpanConversions([ValueSource(nameof(roslyn5OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task ExpandParamsArgumentsDisabled([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions)
{

69
ICSharpCode.Decompiler.Tests/Semantics/ConversionTests.cs

@ -1787,5 +1787,74 @@ namespace ICSharpCode.Decompiler.Tests.Semantics @@ -1787,5 +1787,74 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
return conversions.ImplicitConversion(bodyReturnType, returnType);
}
}
#region First-class span conversions
Conversion SpanConversion(Type from, Type to)
{
var c = RefAssemblyCompilation.Instance;
return CSharpConversions.Get(c).ImplicitConversion(c.FindType(from), c.FindType(to));
}
[Test]
public void ImplicitSpanConversions()
{
Assert.That(SpanConversion(typeof(string), typeof(ReadOnlySpan<char>)), Is.EqualTo(C.ImplicitSpanConversion),
"the span conversion must win over String's own op_Implicit: user-defined conversions are not considered between span-convertible types");
Assert.That(SpanConversion(typeof(string[]), typeof(Span<string>)), Is.EqualTo(C.ImplicitSpanConversion));
Assert.That(SpanConversion(typeof(string[]), typeof(ReadOnlySpan<object>)), Is.EqualTo(C.ImplicitSpanConversion));
Assert.That(SpanConversion(typeof(Span<string>), typeof(ReadOnlySpan<object>)), Is.EqualTo(C.ImplicitSpanConversion));
Assert.That(SpanConversion(typeof(ReadOnlySpan<string>), typeof(ReadOnlySpan<object>)), Is.EqualTo(C.ImplicitSpanConversion));
}
[Test]
public void NoImplicitSpanConversionWithoutElementCovariance()
{
// Roslyn: CS0029 - no conversion at all relates these.
Assert.That(SpanConversion(typeof(int[]), typeof(ReadOnlySpan<long>)), Is.EqualTo(C.None));
// Roslyn: CS0266 - only an EXPLICIT (span) conversion exists. In particular the
// user-defined route via Span<object>.op_Implicit(object[]) plus array covariance
// must not be considered, because a span conversion exists for the pair.
Assert.That(SpanConversion(typeof(string[]), typeof(Span<object>)), Is.EqualTo(C.None));
}
Conversion SpanMethodGroupConversion(ResolveResult target, Type delegateType)
{
var c = RefAssemblyCompilation.Instance;
var extensionMethod = c.FindType(typeof(SpanReceiverExtensionTestCase))
.GetMethods(m => m.Name == "M").Single();
var mgrr = new MethodGroupResolveResult(
target, "M",
new[] { new MethodListWithDeclaringType(target.Type, target.Type.GetMethods(m => m.Name == "M")) },
typeArguments: null);
mgrr.extensionMethods = new List<List<IMethod>> { new List<IMethod> { extensionMethod } };
return CSharpConversions.Get(c).ImplicitConversion(mgrr, c.FindType(delegateType));
}
[Test]
public void MethodGroupConversion_SpanConversionOnTheReceiverIsNotConsidered()
{
// C# 14 first-class spans: "span conversion is not considered when overload
// resolution is performed for a method group conversion". For 'str.M' with M being
// an extension on ReadOnlySpan<char>, Roslyn reports CS0123, even though the
// invocation 'str.M()' is legal.
var c = RefAssemblyCompilation.Instance;
var conversion = SpanMethodGroupConversion(
new ResolveResult(c.FindType(KnownTypeCode.String)), typeof(Action));
Assert.That(conversion, Is.EqualTo(C.None));
}
[Test]
public void MethodGroupConversion_IdentityReceiverOnSpanExtensionStillConverts()
{
// Guard for the rule above: with an identity-typed receiver the method group
// conversion stays legal.
var c = RefAssemblyCompilation.Instance;
var conversion = SpanMethodGroupConversion(
new ResolveResult(c.FindType(typeof(ReadOnlySpan<char>))), typeof(Action));
Assert.That(conversion.IsMethodGroupConversion);
Assert.That(conversion.IsValid);
}
#endregion
}
}

38
ICSharpCode.Decompiler.Tests/Semantics/ExplicitConversionTest.cs

@ -766,5 +766,43 @@ namespace ICSharpCode.Decompiler.Tests.Semantics @@ -766,5 +766,43 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
Assert.That(conversions.ExplicitConversion(compilation.FindType(KnownTypeCode.Object), t), Is.EqualTo(C.ExplicitReferenceConversion));
Assert.That(conversions.ExplicitConversion(t, compilation.FindType(typeof(IConvertible))), Is.EqualTo(C.ExplicitReferenceConversion));
}
#region First-class span conversions
Conversion SpanExplicitConversion(Type from, Type to)
{
var c = RefAssemblyCompilation.Instance;
return CSharpConversions.Get(c).ExplicitConversion(c.FindType(from), c.FindType(to));
}
[Test]
public void ExplicitSpanConversion_CovariantArrayToSpan()
{
// C# 14 first-class spans: an explicit span conversion exists from an array to
// Span<U>/ReadOnlySpan<U> when an explicit reference conversion relates the element
// types, and user-defined operators are not considered for such pairs. Roslyn
// compiles '(Span<string>)objectArray', and reports CS0266 (explicit conversion
// exists) for 'Span<object> s = stringArray;'.
var downcast = SpanExplicitConversion(typeof(object[]), typeof(Span<string>));
Assert.That(downcast.IsValid);
Assert.That(!downcast.IsUserDefined);
var downcastRos = SpanExplicitConversion(typeof(object[]), typeof(ReadOnlySpan<string>));
Assert.That(downcastRos.IsValid);
Assert.That(!downcastRos.IsUserDefined);
var upcast = SpanExplicitConversion(typeof(string[]), typeof(Span<object>));
Assert.That(upcast.IsValid);
Assert.That(!upcast.IsUserDefined);
}
[Test]
public void NoExplicitSpanConversionWithoutElementReferenceConversion()
{
// Roslyn: CS0030 - int[] and Span<long>/ReadOnlySpan<long> are unrelated; the
// user-defined operator route (op_Implicit(long[])) must not resurrect the cast.
Assert.That(!SpanExplicitConversion(typeof(int[]), typeof(Span<long>)).IsValid);
Assert.That(!SpanExplicitConversion(typeof(int[]), typeof(ReadOnlySpan<long>)).IsValid);
}
#endregion
}
}

175
ICSharpCode.Decompiler.Tests/Semantics/OverloadResolutionTests.cs

@ -18,10 +18,12 @@ @@ -18,10 +18,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using ICSharpCode.Decompiler.CSharp.Resolver;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.Semantics;
using ICSharpCode.Decompiler.Tests.TypeSystem;
using ICSharpCode.Decompiler.TypeSystem;
@ -347,5 +349,178 @@ namespace ICSharpCode.Decompiler.Tests.Semantics @@ -347,5 +349,178 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
Method(a => a.ToString());
}
}
#region First-class span betterness
public struct ConvertibleToBothReadOnlySpans
{
public static implicit operator ReadOnlySpan<int>(ConvertibleToBothReadOnlySpans c)
{
return default;
}
public static implicit operator ReadOnlySpan<long>(ConvertibleToBothReadOnlySpans c)
{
return default;
}
}
static IMethod MakeMethodIn(ICompilation c, params Type[] parameterTypes)
{
var m = new FakeMethod(c, SymbolKind.Method);
m.Name = "Method";
m.Parameters = parameterTypes
.Select(t => (IParameter)new DefaultParameter(c.FindType(t), string.Empty, owner: m))
.ToList();
return m;
}
[Test]
public void ReadOnlySpanOverloadsWithUnrelatedElementTypesAreAmbiguous()
{
// C# 14 spec, 12.6.4.7: ReadOnlySpan<E1> is a better conversion target than
// ReadOnlySpan<E2> only if an implicit conversion exists from ReadOnlySpan<E1>
// to ReadOnlySpan<E2> - the span types, not the element types. No span conversion
// relates ReadOnlySpan<int> and ReadOnlySpan<long>, so neither target is better
// and the call is ambiguous; Roslyn reports CS0121.
var c = RefAssemblyCompilation.Instance;
var r = new OverloadResolution(c, new[] {
new ResolveResult(c.FindType(typeof(ConvertibleToBothReadOnlySpans)))
});
Assert.That(r.AddCandidate(MakeMethodIn(c, typeof(ReadOnlySpan<int>))), Is.EqualTo(OverloadResolutionErrors.None));
Assert.That(r.AddCandidate(MakeMethodIn(c, typeof(ReadOnlySpan<long>))), Is.EqualTo(OverloadResolutionErrors.None));
Assert.That(r.IsAmbiguous);
}
static IMethod MakeByRefMethodIn(ICompilation c, Type parameterType, ReferenceKind kind)
{
var m = new FakeMethod(c, SymbolKind.Method);
m.Name = "Method";
m.Parameters = new List<IParameter> {
new DefaultParameter(new ByReferenceType(c.FindType(parameterType)), string.Empty,
owner: m, referenceKind: kind)
};
return m;
}
static IMethod MakeInMethodIn(ICompilation c, Type parameterType)
=> MakeByRefMethodIn(c, parameterType, ReferenceKind.In);
[Test]
public void InReadOnlySpanParameter_BindsAnArrayWithoutInButNotWithIn()
{
// Roslyn: OnlyIn(arr) compiles - a value argument may bind to an 'in' parameter
// through the implicit span conversion (a temporary is created). OnlyIn(in arr)
// is CS1503: an explicit 'in' argument must have the parameter's own type.
var c = RefAssemblyCompilation.Instance;
var arrayArg = new ResolveResult(new ArrayType(c, c.FindType(KnownTypeCode.Int32)));
var implicitIn = new OverloadResolution(c, new[] { arrayArg });
Assert.That(implicitIn.AddCandidate(MakeInMethodIn(c, typeof(ReadOnlySpan<int>))),
Is.EqualTo(OverloadResolutionErrors.None));
var explicitIn = new OverloadResolution(c, new[] {
new ByReferenceResolveResult(arrayArg, ReferenceKind.In)
});
Assert.That(explicitIn.AddCandidate(MakeInMethodIn(c, typeof(ReadOnlySpan<int>))),
Is.Not.EqualTo(OverloadResolutionErrors.None));
}
[Test]
public void InReadOnlySpanParameter_BindsAnIdentityArgumentWithAndWithoutIn()
{
var c = RefAssemblyCompilation.Instance;
var rosArg = new ResolveResult(c.FindType(typeof(ReadOnlySpan<int>)));
var implicitIn = new OverloadResolution(c, new[] { rosArg });
Assert.That(implicitIn.AddCandidate(MakeInMethodIn(c, typeof(ReadOnlySpan<int>))),
Is.EqualTo(OverloadResolutionErrors.None));
var explicitIn = new OverloadResolution(c, new[] {
new ByReferenceResolveResult(rosArg, ReferenceKind.In)
});
Assert.That(explicitIn.AddCandidate(MakeInMethodIn(c, typeof(ReadOnlySpan<int>))),
Is.EqualTo(OverloadResolutionErrors.None));
}
[Test]
public void ByValueOverloadPreferredOverInOverload_WithoutInAtTheCall()
{
// Roslyn: for F(ReadOnlySpan<int>) vs F(in ReadOnlySpan<int>), a call without 'in'
// picks the by-value overload - both for an identity argument and through the
// span conversion from int[].
var c = RefAssemblyCompilation.Instance;
foreach (var arg in new[] {
new ResolveResult(c.FindType(typeof(ReadOnlySpan<int>))),
new ResolveResult(new ArrayType(c, c.FindType(KnownTypeCode.Int32)))
})
{
var r = new OverloadResolution(c, new[] { arg });
var byValue = MakeMethodIn(c, typeof(ReadOnlySpan<int>));
Assert.That(r.AddCandidate(byValue), Is.EqualTo(OverloadResolutionErrors.None));
Assert.That(r.AddCandidate(MakeInMethodIn(c, typeof(ReadOnlySpan<int>))),
Is.EqualTo(OverloadResolutionErrors.None));
Assert.That(!r.IsAmbiguous, $"argument {arg.Type}");
Assert.That(r.BestCandidate, Is.SameAs(byValue), $"argument {arg.Type}");
}
}
[Test]
public void RefAndOutParametersNeverBindThroughASpanConversion()
{
// Roslyn: CS1503 for both 'M(ref arr)' against 'ref ReadOnlySpan<int>' and
// 'M(out arr)' against 'out ReadOnlySpan<int>' - ref and out demand the
// parameter's own type; the span conversion does not apply. A value argument
// without the keyword is a passing-mode mismatch regardless of conversions.
var c = RefAssemblyCompilation.Instance;
var arrayArg = new ResolveResult(new ArrayType(c, c.FindType(KnownTypeCode.Int32)));
foreach (var kind in new[] { ReferenceKind.Ref, ReferenceKind.Out })
{
var byRefArgument = new OverloadResolution(c, new[] {
new ByReferenceResolveResult(arrayArg, kind)
});
Assert.That(byRefArgument.AddCandidate(MakeByRefMethodIn(c, typeof(ReadOnlySpan<int>), kind)),
Is.Not.EqualTo(OverloadResolutionErrors.None), kind.ToString());
var valueArgument = new OverloadResolution(c, new[] { arrayArg });
Assert.That(valueArgument.AddCandidate(MakeByRefMethodIn(c, typeof(ReadOnlySpan<int>), kind)),
Is.Not.EqualTo(OverloadResolutionErrors.None), kind.ToString());
}
}
[Test]
public void InOverloadIsTheOnlyCandidateWithInAtTheCall()
{
// Roslyn: F(in ros) picks the in-overload; the by-value overload cannot take an
// 'in' argument.
var c = RefAssemblyCompilation.Instance;
var r = new OverloadResolution(c, new[] {
new ByReferenceResolveResult(new ResolveResult(c.FindType(typeof(ReadOnlySpan<int>))), ReferenceKind.In)
});
Assert.That(r.AddCandidate(MakeMethodIn(c, typeof(ReadOnlySpan<int>))),
Is.Not.EqualTo(OverloadResolutionErrors.None));
var inOverload = MakeInMethodIn(c, typeof(ReadOnlySpan<int>));
Assert.That(r.AddCandidate(inOverload), Is.EqualTo(OverloadResolutionErrors.None));
Assert.That(r.BestCandidate, Is.SameAs(inOverload));
}
[Test]
public void ReadOnlySpanOfStringPreferredOverReadOnlySpanOfObject()
{
// The positive direction of the same rule: string[] converts to both targets, and
// the covariant span conversion ReadOnlySpan<string> -> ReadOnlySpan<object>
// exists, so ReadOnlySpan<string> is the better target.
var c = RefAssemblyCompilation.Instance;
var r = new OverloadResolution(c, new[] {
new ResolveResult(new ArrayType(c, c.FindType(KnownTypeCode.String)))
});
var better = MakeMethodIn(c, typeof(ReadOnlySpan<string>));
Assert.That(r.AddCandidate(better), Is.EqualTo(OverloadResolutionErrors.None));
Assert.That(r.AddCandidate(MakeMethodIn(c, typeof(ReadOnlySpan<object>))), Is.EqualTo(OverloadResolutionErrors.None));
Assert.That(!r.IsAmbiguous);
Assert.That(r.BestCandidate, Is.SameAs(better));
}
#endregion
}
}

48
ICSharpCode.Decompiler.Tests/Semantics/RefAssemblyCompilation.cs

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
// Copyright (c) 2026 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.IO;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.Tests.Helpers;
using ICSharpCode.Decompiler.Tests.TypeSystem;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.TypeSystem.Implementation;
namespace ICSharpCode.Decompiler.Tests.Semantics
{
/// <summary>
/// A compilation over a .NET reference assembly plus the test assembly, shared by all tests
/// whose subject types postdate the legacy mscorlib the main test compilation resolves
/// against - System.ValueTuple and Span&lt;T&gt;/ReadOnlySpan&lt;T&gt;. The reference assembly
/// is read once for the whole test run.
/// </summary>
static class RefAssemblyCompilation
{
public static ICompilation Instance => instance.Value;
static readonly Lazy<ICompilation> instance = new Lazy<ICompilation>(
delegate {
string path = Path.Combine(
Tester.RefAssembliesToolset.GetPath(".NETCoreApp,Version=v5.0"), "System.Runtime.dll");
return new SimpleCompilation(TypeSystemLoaderTests.TestAssembly,
new PEFile(path, new FileStream(path, FileMode.Open, FileAccess.Read)));
});
}
}

123
ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs

@ -67,14 +67,6 @@ namespace ICSharpCode.Decompiler.Tests.Semantics @@ -67,14 +67,6 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
ICompilation compilation;
TypeInference ti;
// The legacy reference mscorlib used by the main compilation predates
// System.ValueTuple, so tuple-related tests resolve against a .NET ref assembly.
static readonly Lazy<ICompilation> tupleCompilation = new Lazy<ICompilation>(
delegate {
string path = Path.Combine(Helpers.Tester.RefAssembliesToolset.GetPath(".NETCoreApp,Version=v5.0"), "System.Runtime.dll");
return new SimpleCompilation(new PEFile(path, new FileStream(path, FileMode.Open, FileAccess.Read)));
});
[OneTimeSetUp]
public void OneTimeSetUp()
{
@ -689,7 +681,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics @@ -689,7 +681,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
// each element to the corresponding element type, giving the bounds
// { int, long } and the fixed type long. Treating the literal like a value
// of type (int, long) would instead produce conflicting exact bounds.
var comp = tupleCompilation.Value;
var comp = RefAssemblyCompilation.Instance;
var inference = new TypeInference(comp);
var T = new DefaultTypeParameter(comp, SymbolKind.Method, 0, "T");
var tupleOfTT = new TupleType(comp, ImmutableArray.Create<IType>(T, T));
@ -725,7 +717,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics @@ -725,7 +717,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
public void BestCommonTypeMergesTupleElementNames()
{
// var m = cond ? (a: 1, b: "x") : (a: 2, c: "y"); -> (int a, string)
var comp = tupleCompilation.Value;
var comp = RefAssemblyCompilation.Instance;
var inference = new TypeInference(comp);
bool success;
@ -743,7 +735,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics @@ -743,7 +735,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
{
// Signature: M<T>(IList<T> x, T y)
// Invocation: M(listOfAB, valueAC); -> T = (int a, string)
var comp = tupleCompilation.Value;
var comp = RefAssemblyCompilation.Instance;
var inference = new TypeInference(comp);
var T = new DefaultTypeParameter(comp, SymbolKind.Method, 0, "T");
ITypeDefinition listType = comp.FindType(KnownTypeCode.IListOfT).GetDefinition();
@ -770,7 +762,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics @@ -770,7 +762,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
// Signature: M<T>(T x, T y)
// Invocation: M(listOfAB, listOfAC); -> T = IList<(int a, string)>
// M(arrayOfAB, arrayOfAC); -> T = (int a, string)[]
var comp = tupleCompilation.Value;
var comp = RefAssemblyCompilation.Instance;
ITypeDefinition listType = comp.FindType(KnownTypeCode.IListOfT).GetDefinition();
IType InferSingle(IType argType1, IType argType2)
@ -804,7 +796,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics @@ -804,7 +796,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
// Invocation: M(listOfAB, actionOfListOfAC); -> T = IList<(int a, string)>
// Action<in T> is contravariant, so the second argument produces an upper bound
// while the first produces a lower bound.
var comp = tupleCompilation.Value;
var comp = RefAssemblyCompilation.Instance;
var inference = new TypeInference(comp);
var T = new DefaultTypeParameter(comp, SymbolKind.Method, 0, "T");
ITypeDefinition listType = comp.FindType(KnownTypeCode.IListOfT).GetDefinition();
@ -833,7 +825,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics @@ -833,7 +825,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
// Signature: M<T>(T x, T y)
// Invocation: M(nullableListOfAB, nullableListOfAC); -> T = IList<(int a, string)>?
// M(nullableArrayOfAB, nullableArrayOfAC); -> T = (int a, string)[]?
var comp = tupleCompilation.Value;
var comp = RefAssemblyCompilation.Instance;
ITypeDefinition listType = comp.FindType(KnownTypeCode.IListOfT).GetDefinition();
IType InferSingle(IType argType1, IType argType2)
@ -869,7 +861,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics @@ -869,7 +861,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
// Merging nullability requires the variance of the position, which this
// implementation does not track, so such bounds stay distinct and fixing fails
// (csc infers string[]?).
var comp = tupleCompilation.Value;
var comp = RefAssemblyCompilation.Instance;
var T = new DefaultTypeParameter(comp, SymbolKind.Method, 0, "T");
IType stringType = comp.FindType(KnownTypeCode.String);
@ -888,7 +880,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics @@ -888,7 +880,7 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
{
// Signature: M<T>(ref T x, ref T y)
// Invocation: M(ref ab, ref ac); -> T = (int a, string)
var comp = tupleCompilation.Value;
var comp = RefAssemblyCompilation.Instance;
var inference = new TypeInference(comp);
var T = new DefaultTypeParameter(comp, SymbolKind.Method, 0, "T");
@ -1361,5 +1353,104 @@ namespace ICSharpCode.Decompiler.Tests.Semantics @@ -1361,5 +1353,104 @@ namespace ICSharpCode.Decompiler.Tests.Semantics
Is.EqualTo(Resolve(typeof(List<string>), typeof(List<Version>), typeof(Collection<string>), typeof(Collection<Version>), typeof(ReadOnlyCollection<string>), typeof(ReadOnlyCollection<Version>), typeof(System.Runtime.CompilerServices.ReadOnlyCollectionBuilder<string>), typeof(System.Runtime.CompilerServices.ReadOnlyCollectionBuilder<Version>))));
}
#endregion
#region First-class span type inference
IType[] InferSpan(Func<ICompilation, ITypeParameter, IType[]> parameterTypes,
Func<ICompilation, ResolveResult[]> arguments, out bool success)
{
var c = RefAssemblyCompilation.Instance;
var inference = new TypeInference(c);
ITypeParameter tp = new DefaultTypeParameter(c, SymbolKind.Method, 0, "T");
return inference.InferTypeArguments(new[] { tp }, arguments(c), parameterTypes(c, tp), out success);
}
static ParameterizedType SpanOf(ICompilation c, IType element)
=> new ParameterizedType(c.FindType(KnownTypeCode.SpanOfT).GetDefinition(), new[] { element });
static ParameterizedType ReadOnlySpanOf(ICompilation c, IType element)
=> new ParameterizedType(c.FindType(KnownTypeCode.ReadOnlySpanOfT).GetDefinition(), new[] { element });
[Test]
public void SpanArgumentAloneInfersItsElementType()
{
bool success;
Assert.That(
InferSpan(
(c, tp) => new IType[] { SpanOf(c, tp) },
c => new[] { new ResolveResult(SpanOf(c, c.FindType(KnownTypeCode.String))) },
out success),
Is.EqualTo(new[] { RefAssemblyCompilation.Instance.FindType(KnownTypeCode.String) }));
Assert.That(success);
}
[Test]
public void SpanArgumentGivesAnExactBound_ConflictingLowerBoundFailsInference()
{
// M<T>(Span<T>, T) called with (Span<string>, object): Span<T> is invariant, so the
// span argument contributes an EXACT bound (C# 14 spec, 12.6.3.10: "If V is a
// Span<V1>, then an exact inference is made"). The conflicting lower bound object
// must fail inference; Roslyn reports CS0411 for this call.
bool success;
InferSpan(
(c, tp) => new IType[] { SpanOf(c, tp), tp },
c => new[] {
new ResolveResult(SpanOf(c, c.FindType(KnownTypeCode.String))),
new ResolveResult(c.FindType(KnownTypeCode.Object))
},
out success);
Assert.That(success, Is.False);
}
[Test]
public void ArrayArgumentForSpanParameterGivesAnExactBound_ConflictingLowerBoundFailsInference()
{
// Same as above with a string[] argument: the array-to-Span conversion requires
// identity element types, so the bound is exact. Roslyn reports CS0411.
bool success;
InferSpan(
(c, tp) => new IType[] { SpanOf(c, tp), tp },
c => new[] {
new ResolveResult(new ArrayType(c, c.FindType(KnownTypeCode.String))),
new ResolveResult(c.FindType(KnownTypeCode.Object))
},
out success);
Assert.That(success, Is.False);
}
[Test]
public void SpanArgumentForReadOnlySpanParameterGivesALowerBound()
{
// M<T>(ReadOnlySpan<T>, T) called with (Span<string>, object): ReadOnlySpan is
// covariance-convertible, the span argument contributes a LOWER bound, and T=object
// wins. Roslyn compiles this with T=object.
bool success;
Assert.That(
InferSpan(
(c, tp) => new IType[] { ReadOnlySpanOf(c, tp), tp },
c => new[] {
new ResolveResult(SpanOf(c, c.FindType(KnownTypeCode.String))),
new ResolveResult(c.FindType(KnownTypeCode.Object))
},
out success),
Is.EqualTo(new[] { RefAssemblyCompilation.Instance.FindType(KnownTypeCode.Object) }));
Assert.That(success);
}
[Test]
public void ArrayArgumentForReadOnlySpanParameterGivesALowerBound()
{
bool success;
Assert.That(
InferSpan(
(c, tp) => new IType[] { ReadOnlySpanOf(c, tp), tp },
c => new[] {
new ResolveResult(new ArrayType(c, c.FindType(KnownTypeCode.String))),
new ResolveResult(c.FindType(KnownTypeCode.Object))
},
out success),
Is.EqualTo(new[] { RefAssemblyCompilation.Instance.FindType(KnownTypeCode.Object) }));
Assert.That(success);
}
#endregion
}
}

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
}
}

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

@ -0,0 +1,152 @@ @@ -0,0 +1,152 @@
// Copyright (c) 2026 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal static class FirstClassSpanConversions
{
internal class Base
{
}
internal class Derived : Base
{
}
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)
{
}
public static void AcceptReadOnlySpanBase(ReadOnlySpan<Base> s)
{
}
public static void AcceptInReadOnlySpan(in ReadOnlySpan<int> s)
{
}
public static void ObjectOrReadOnlySpanChar(object a)
{
}
public static void ObjectOrReadOnlySpanChar(ReadOnlySpan<char> a)
{
}
public static void StringArgument(string s)
{
AcceptReadOnlySpanChar(s);
ObjectOrReadOnlySpanChar(s);
s.ExtensionOnReadOnlySpanChar();
}
public static ReadOnlySpan<char> StringToReadOnlySpanCharReturn(string s)
{
return s;
}
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 + readOnlySpan.Length;
}
public static void VarianceReadOnlySpan(ReadOnlySpan<Derived> s)
{
AcceptReadOnlySpanBase(s);
}
public static void VarianceSpan(Span<Derived> s)
{
AcceptReadOnlySpanBase(s);
}
public static ReadOnlySpan<Base> VarianceReturn(ReadOnlySpan<Derived> s)
{
return s;
}
public static void CovariantArrayToReadOnlySpan(Derived[] a)
{
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);
}
public static void ByValueOrIn(ReadOnlySpan<int> s)
{
}
public static void ByValueOrIn(in ReadOnlySpan<int> s)
{
}
public static void CallByValueOrIn(ReadOnlySpan<int> s, int[] a)
{
// Without 'in' the by-value overload is the better parameter-passing choice, also
// through the span conversion; with 'in' only the in-overload binds, so the
// keyword must survive decompilation.
ByValueOrIn(s);
ByValueOrIn(in s);
ByValueOrIn(a);
}
}
internal static class FirstClassSpanConversionsExtensions
{
public static void ExtensionOnReadOnlySpanChar(this ReadOnlySpan<char> s)
{
}
}
}

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

@ -0,0 +1,216 @@ @@ -0,0 +1,216 @@
// Copyright (c) 2026 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal static class FirstClassSpanTypes
{
public static void ArrayOrReadOnlySpan(int[] a)
{
}
public static void ArrayOrReadOnlySpan(ReadOnlySpan<int> a)
{
}
public static void ArrayOrSpan(int[] a)
{
}
public static void ArrayOrSpan(Span<int> a)
{
}
public static void SpanOrReadOnlySpan(Span<int> a)
{
}
public static void SpanOrReadOnlySpan(ReadOnlySpan<int> a)
{
}
public static void ObjectOrReadOnlySpan(object a)
{
}
public static void ObjectOrReadOnlySpan(ReadOnlySpan<int> a)
{
}
public static void ObjectOrReadOnlySpanChar(object a)
{
}
public static void ObjectOrReadOnlySpanChar(ReadOnlySpan<char> a)
{
}
public static void EnumerableOrReadOnlySpan(IEnumerable<int> a)
{
}
public static void EnumerableOrReadOnlySpan(ReadOnlySpan<int> a)
{
}
public static void CovariantArrayOrReadOnlySpan(object[] a)
{
}
public static void CovariantArrayOrReadOnlySpan(ReadOnlySpan<string> a)
{
}
public static void ReadOnlySpanOfObjectOrString(ReadOnlySpan<object> a)
{
}
public static void ReadOnlySpanOfObjectOrString(ReadOnlySpan<string> a)
{
}
public static void StringOrReadOnlySpanChar(string a)
{
}
public static void StringOrReadOnlySpanChar(ReadOnlySpan<char> a)
{
}
public static void ParamsArrayOrParamsReadOnlySpan(params int[] a)
{
}
public static void ParamsArrayOrParamsReadOnlySpan(params ReadOnlySpan<int> a)
{
}
public static void RefSpanOrByValue(ref ReadOnlySpan<int> s)
{
}
public static void RefSpanOrByValue(ReadOnlySpan<int> s)
{
}
public static void OutSpanOrByValue(out ReadOnlySpan<int> s)
{
s = default(ReadOnlySpan<int>);
}
public static void OutSpanOrByValue(ReadOnlySpan<int> s)
{
}
public static void GenericArrayOrReadOnlySpan<T>(T[] a)
{
}
public static void GenericArrayOrReadOnlySpan<T>(ReadOnlySpan<T> a)
{
}
public static void InferFromReadOnlySpan<T>(ReadOnlySpan<T> a)
{
}
public static ReadOnlySpan<int> ArrayToReadOnlySpanReturn(int[] a)
{
return a;
}
public static ReadOnlySpan<int> TernaryArrayOrSpan(bool b, int[] a, Span<int> s)
{
#if OPT
if (!b)
{
return s;
}
return a;
#else
return b ? ((ReadOnlySpan<int>)a) : ((ReadOnlySpan<int>)s);
#endif
}
public static bool SpanExtensionContains(int[] a)
{
// binds to MemoryExtensions.Contains under C# 14 first-class span conversions
return a.Contains(2);
}
public static bool LinqContains(int[] a)
{
// Enumerable.Contains loses against MemoryExtensions.Contains under C# 14;
// extension method syntax must not be used here
return Enumerable.Contains(a, 2);
}
public static void CallWinners(int[] arr, Span<int> span, string str, string[] strArr)
{
ArrayOrReadOnlySpan(arr);
ArrayOrSpan(arr);
SpanOrReadOnlySpan(arr);
SpanOrReadOnlySpan(span);
ObjectOrReadOnlySpan(arr);
EnumerableOrReadOnlySpan(arr);
CovariantArrayOrReadOnlySpan(strArr);
ReadOnlySpanOfObjectOrString(strArr);
StringOrReadOnlySpanChar(str);
ParamsArrayOrParamsReadOnlySpan(arr);
ParamsArrayOrParamsReadOnlySpan(1, 2, 3);
GenericArrayOrReadOnlySpan(arr);
InferFromReadOnlySpan(arr);
InferFromReadOnlySpan(span);
arr.ExtensionOnReadOnlySpan();
}
public static void CallRefOutOrByValue(int[] arr)
{
// A span conversion never binds a ref or out parameter: without the keyword the
// by-value overload wins, with the keyword only the ref/out overload is
// applicable and the keyword must survive decompilation.
ReadOnlySpan<int> s = arr;
RefSpanOrByValue(arr);
RefSpanOrByValue(ref s);
OutSpanOrByValue(arr);
OutSpanOrByValue(out s);
}
public static void CallLosersWithExplicitConversions(int[] arr, string str)
{
ArrayOrReadOnlySpan((ReadOnlySpan<int>)arr);
ArrayOrSpan((Span<int>)arr);
SpanOrReadOnlySpan((Span<int>)arr);
ObjectOrReadOnlySpan((object)arr);
ObjectOrReadOnlySpanChar((object)str);
EnumerableOrReadOnlySpan((IEnumerable<int>)arr);
StringOrReadOnlySpanChar((ReadOnlySpan<char>)str);
}
}
internal static class FirstClassSpanTypesExtensions
{
public static void ExtensionOnReadOnlySpan<T>(this ReadOnlySpan<T> s)
{
}
}
}

12
ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs

@ -601,6 +601,18 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem @@ -601,6 +601,18 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem
public void Dispose() { }
}
/// <summary>
/// Extension method on a span receiver, for the method-group-conversion span tests:
/// an invocation may reach it through the implicit span conversion of the receiver,
/// a method group conversion may not.
/// </summary>
public static class SpanReceiverExtensionTestCase
{
public static void M(this ReadOnlySpan<char> receiver)
{
}
}
/// <summary>
/// Fixtures for ConversionTest.MethodGroupConversion_* tests: delegate types and
/// per-test method sets, resolved through hand-built MethodGroupResolveResults.

74
ICSharpCode.Decompiler/CSharp/CallBuilder.cs

@ -329,6 +329,46 @@ namespace ICSharpCode.Decompiler.CSharp @@ -329,6 +329,46 @@ 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);
}
// Gets whether a call to `method` is equivalent to an implicit span conversion.
static bool IsEquivalentToSpanConversion(IMethod method)
{
if (method.DeclaringType.IsKnownType(KnownTypeCode.SpanOfT)
|| method.DeclaringType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
{
if (method.IsOperator
&& method.Name == "op_Implicit")
{
return true;
}
}
return IsStringToReadOnlySpanCharAsSpan(method)
|| IsReadOnlySpanCastUp(method);
}
public ExpressionWithResolveResult Build(OpCode callOpCode, IMethod method,
IReadOnlyList<ILInstruction> callArguments,
IReadOnlyList<int>? argumentToParameterMap = null,
@ -481,6 +521,21 @@ namespace ICSharpCode.Decompiler.CSharp @@ -481,6 +521,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 +1079,15 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1024,6 +1079,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 +1599,15 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1535,7 +1599,15 @@ 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 directlyConvertible = conv.IsValid
&& (conv.IsUserDefined
? conv.Method.Equals(method, NormalizeTypeVisitor.TypeErasure)
: conv.IsImplicitSpanConversion && IsEquivalentToSpanConversion(method));
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);

87
ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs

@ -326,6 +326,8 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver @@ -326,6 +326,8 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
return c;
if (ExplicitReferenceConversion(fromType, toType))
return Conversion.ExplicitReferenceConversion;
if (IsExplicitSpanConversion(fromType, toType))
return Conversion.ExplicitSpanConversion;
if (UnboxingConversion(fromType, toType))
return Conversion.UnboxingConversion;
c = ExplicitTypeParameterConversion(fromType, toType);
@ -1014,6 +1016,15 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver @@ -1014,6 +1016,15 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
return Conversion.None;
}
// C# 14: user-defined conversions are not considered when converting between types
// for which an implicit or an explicit span conversion exists. In particular,
// string[] must not reach Span<object> through op_Implicit(object[]) plus array
// covariance - the pair only has the explicit span conversion.
if (IsImplicitSpanConversion(fromType, toType) || IsExplicitSpanConversion(fromType, toType))
{
return Conversion.None;
}
var operators = GetApplicableConversionOperators(fromResult, fromType, toType, false);
if (operators.Count > 0)
@ -1063,6 +1074,13 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver @@ -1063,6 +1074,13 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
return Conversion.None;
}
// C# 14: user-defined conversions are not considered when converting between types
// for which an implicit or an explicit span conversion exists.
if (IsImplicitSpanConversion(fromType, toType) || IsExplicitSpanConversion(fromType, toType))
{
return Conversion.None;
}
var operators = GetApplicableConversionOperators(fromResult, fromType, toType, true);
if (operators.Count > 0)
{
@ -1251,6 +1269,31 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver @@ -1251,6 +1269,31 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
return false;
}
/// <summary>
/// C# 14 explicit span conversion: from a single-dimensional array to Span&lt;U&gt; or
/// ReadOnlySpan&lt;U&gt; where an explicit reference conversion relates the element types.
/// The explicit conversions include the implicit ones, so element covariance that is
/// not an identity conversion (string[] to Span&lt;object&gt;) also lands here.
/// </summary>
bool IsExplicitSpanConversion(IType fromType, IType toType)
{
if (!compilation.TypeSystemOptions.HasFlag(TypeSystemOptions.FirstClassSpanTypes))
{
return false;
}
if (fromType is ArrayType { Dimensions: 1, ElementType: var elementType }
&& (toType.IsKnownType(KnownTypeCode.SpanOfT) || toType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)))
{
IType spanElementType = toType.TypeArguments[0];
return IdentityConversion(elementType, spanElementType)
|| IsImplicitReferenceConversion(elementType, spanElementType)
|| ExplicitReferenceConversion(elementType, spanElementType);
}
return false;
}
#endregion
#region AnonymousFunctionConversion
@ -1370,7 +1413,10 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver @@ -1370,7 +1413,10 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
allowExpandingParams: false,
allowOptionalParameters: false,
allowImplicitIn: false,
conversions: this
conversions: this,
// C# 14 first-class spans: "span conversion is not considered when overload
// resolution is performed for a method group conversion".
allowSpanConversionOnExtensionReceiver: false
);
if (or.FoundApplicableCandidate)
{
@ -1657,36 +1703,21 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver @@ -1657,36 +1703,21 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
/// <returns>0 = neither is better; 1 = t1 is better; 2 = t2 is better</returns>
int BetterConversionTarget(IType t1, IType t2)
{
if (t1.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
{
if (t2.IsKnownType(KnownTypeCode.SpanOfT))
{
if (IdentityConversion(t1.TypeArguments[0], t2.TypeArguments[0]))
return 1;
}
if (t2.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
{
bool t1To2 = ImplicitConversion(t1.TypeArguments[0], t2.TypeArguments[0]).IsValid;
bool t2To1 = ImplicitConversion(t2.TypeArguments[0], t1.TypeArguments[0]).IsValid;
if (t1To2 && !t2To1)
return 1;
}
// ReadOnlySpan<E> beats Span<E>. This must pre-empt the mutual-convertibility rule
// below, which would conclude the opposite from the Span-to-ReadOnlySpan conversion.
// The ReadOnlySpan<E1>-vs-ReadOnlySpan<E2> case needs no rule of its own: per the
// C# 14 spec it is decided by implicit convertibility between the SPAN types (not
// the element types), which is exactly what the rule below tests.
if (t1.IsKnownType(KnownTypeCode.ReadOnlySpanOfT) && t2.IsKnownType(KnownTypeCode.SpanOfT))
{
if (IdentityConversion(t1.TypeArguments[0], t2.TypeArguments[0]))
return 1;
}
if (t2.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
if (t2.IsKnownType(KnownTypeCode.ReadOnlySpanOfT) && t1.IsKnownType(KnownTypeCode.SpanOfT))
{
if (t1.IsKnownType(KnownTypeCode.SpanOfT))
{
if (IdentityConversion(t2.TypeArguments[0], t1.TypeArguments[0]))
return 2;
}
if (t1.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
{
bool t1To2 = ImplicitConversion(t1.TypeArguments[0], t2.TypeArguments[0]).IsValid;
bool t2To1 = ImplicitConversion(t2.TypeArguments[0], t1.TypeArguments[0]).IsValid;
if (t2To1 && !t1To2)
return 2;
}
if (IdentityConversion(t2.TypeArguments[0], t1.TypeArguments[0]))
return 2;
}
{

12
ICSharpCode.Decompiler/CSharp/Resolver/OverloadResolution.cs

@ -206,6 +206,13 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver @@ -206,6 +206,13 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
/// </summary>
public bool AllowImplicitIn { get; set; } = true;
/// <summary>
/// Gets/Sets whether an extension method receiver may bind through an implicit span
/// conversion. True for invocations; false when resolving a method group conversion,
/// where C# 14 does not consider span conversions.
/// </summary>
public bool AllowSpanConversionOnExtensionReceiver { get; set; } = true;
/// <summary>
/// Gets/Sets whether ConversionResolveResults created by this OverloadResolution
/// instance apply overflow checking.
@ -711,8 +718,11 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver @@ -711,8 +718,11 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
if (IsExtensionMethodInvocation && parameterIndex == 0)
{
// First parameter to extension method must be an identity, reference, boxing or span conversion
if (!(c == Conversion.IdentityConversion || c == Conversion.ImplicitReferenceConversion || c == Conversion.BoxingConversion || c == Conversion.ImplicitSpanConversion))
if (!(c == Conversion.IdentityConversion || c == Conversion.ImplicitReferenceConversion || c == Conversion.BoxingConversion
|| (c == Conversion.ImplicitSpanConversion && AllowSpanConversionOnExtensionReceiver)))
{
candidate.AddError(OverloadResolutionErrors.ArgumentTypeMismatch);
}
}
else
{

6
ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs

@ -782,11 +782,13 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver @@ -782,11 +782,13 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
case (ArrayType arrU, ArrayType arrV) when arrU.Dimensions == arrV.Dimensions:
MakeLowerBoundInference(arrU.ElementType, arrV.ElementType);
return;
// Span<T> is invariant, so even in a lower-bound context a Span<V1> target
// contributes an exact element inference (C# 14 spec, 12.6.3.10).
case (ArrayType arrU, ParameterizedType spanV) when compilation.TypeSystemOptions.HasFlag(TypeSystemOptions.FirstClassSpanTypes) && spanV.IsKnownType(KnownTypeCode.SpanOfT):
MakeLowerBoundInference(arrU.ElementType, spanV.TypeArguments[0]);
MakeExactInference(arrU.ElementType, spanV.TypeArguments[0]);
return;
case (ParameterizedType spanU, ParameterizedType spanV) when compilation.TypeSystemOptions.HasFlag(TypeSystemOptions.FirstClassSpanTypes) && spanU.IsKnownType(KnownTypeCode.SpanOfT) && spanV.IsKnownType(KnownTypeCode.SpanOfT):
MakeLowerBoundInference(spanU.TypeArguments[0], spanV.TypeArguments[0]);
MakeExactInference(spanU.TypeArguments[0], spanV.TypeArguments[0]);
return;
case (ArrayType arrU, ParameterizedType rosV) when compilation.TypeSystemOptions.HasFlag(TypeSystemOptions.FirstClassSpanTypes) && rosV.IsKnownType(KnownTypeCode.ReadOnlySpanOfT):
MakeLowerBoundInference(arrU.ElementType, rosV.TypeArguments[0]);

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);
}

17
ICSharpCode.Decompiler/Semantics/Conversion.cs

@ -97,6 +97,13 @@ namespace ICSharpCode.Decompiler.Semantics @@ -97,6 +97,13 @@ namespace ICSharpCode.Decompiler.Semantics
/// </summary>
public static readonly Conversion ImplicitSpanConversion = new BuiltinConversion(true, 13);
/// <summary>
/// C# 14 explicit span conversion: from an array type to <see cref="System.Span{T}"/> or
/// <see cref="System.ReadOnlySpan{T}"/> where the element types are related by an
/// explicit reference conversion.
/// </summary>
public static readonly Conversion ExplicitSpanConversion = new BuiltinConversion(false, 14);
public static Conversion UserDefinedConversion(IMethod operatorMethod, bool isImplicit, Conversion conversionBeforeUserDefinedOperator, Conversion conversionAfterUserDefinedOperator, bool isLifted = false, bool isAmbiguous = false)
{
if (operatorMethod == null)
@ -257,6 +264,7 @@ namespace ICSharpCode.Decompiler.Semantics @@ -257,6 +264,7 @@ namespace ICSharpCode.Decompiler.Semantics
public override bool IsInlineArrayConversion => type == 12;
public override bool IsImplicitSpanConversion => type == 13;
public override bool IsExplicitSpanConversion => type == 14;
public override string ToString()
{
@ -296,6 +304,8 @@ namespace ICSharpCode.Decompiler.Semantics @@ -296,6 +304,8 @@ namespace ICSharpCode.Decompiler.Semantics
return "inline array conversion";
case 13:
return "implicit span conversion";
case 14:
return "explicit span conversion";
}
return (isImplicit ? "implicit " : "explicit ") + name + " conversion";
}
@ -643,6 +653,13 @@ namespace ICSharpCode.Decompiler.Semantics @@ -643,6 +653,13 @@ namespace ICSharpCode.Decompiler.Semantics
/// </summary>
public virtual bool IsImplicitSpanConversion => false;
/// <summary>
/// Gets whether this is an explicit span conversion from an array type to <see cref="System.Span{T}"/>
/// or <see cref="System.ReadOnlySpan{T}"/> whose element types are related by an explicit
/// reference conversion.
/// </summary>
public virtual bool IsExplicitSpanConversion => false;
/// <summary>
/// For a tuple conversion, gets the individual tuple element conversions.
/// </summary>

4
ICSharpCode.Decompiler/Semantics/MethodGroupResolveResult.cs

@ -249,7 +249,8 @@ namespace ICSharpCode.Decompiler.Semantics @@ -249,7 +249,8 @@ namespace ICSharpCode.Decompiler.Semantics
bool allowExpandingParams = true,
bool allowOptionalParameters = true,
bool allowImplicitIn = true,
bool checkForOverflow = false, CSharpConversions conversions = null)
bool checkForOverflow = false, CSharpConversions conversions = null,
bool allowSpanConversionOnExtensionReceiver = true)
{
Log.WriteLine("Performing overload resolution for " + this);
Log.WriteCollection(" Arguments: ", arguments);
@ -287,6 +288,7 @@ namespace ICSharpCode.Decompiler.Semantics @@ -287,6 +288,7 @@ namespace ICSharpCode.Decompiler.Semantics
extOr.IsExtensionMethodInvocation = true;
extOr.CheckForOverflow = checkForOverflow;
extOr.AllowImplicitIn = allowImplicitIn;
extOr.AllowSpanConversionOnExtensionReceiver = allowSpanConversionOnExtensionReceiver;
foreach (var g in extensionMethods)
{

Loading…
Cancel
Save