diff --git a/.gitattributes b/.gitattributes index a6227fffc..1d3ed1d12 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 1b4339f43..cba53f058 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -212,6 +212,8 @@ + + diff --git a/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs index 2026ad906..7e9f8df94 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 634aaa5cf..3a16f4270 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -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) { diff --git a/ICSharpCode.Decompiler.Tests/Semantics/ConversionTests.cs b/ICSharpCode.Decompiler.Tests/Semantics/ConversionTests.cs index 3d5308925..ab1bf9af9 100644 --- a/ICSharpCode.Decompiler.Tests/Semantics/ConversionTests.cs +++ b/ICSharpCode.Decompiler.Tests/Semantics/ConversionTests.cs @@ -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)), 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)), Is.EqualTo(C.ImplicitSpanConversion)); + Assert.That(SpanConversion(typeof(string[]), typeof(ReadOnlySpan)), Is.EqualTo(C.ImplicitSpanConversion)); + Assert.That(SpanConversion(typeof(Span), typeof(ReadOnlySpan)), Is.EqualTo(C.ImplicitSpanConversion)); + Assert.That(SpanConversion(typeof(ReadOnlySpan), typeof(ReadOnlySpan)), Is.EqualTo(C.ImplicitSpanConversion)); + } + + [Test] + public void NoImplicitSpanConversionWithoutElementCovariance() + { + // Roslyn: CS0029 - no conversion at all relates these. + Assert.That(SpanConversion(typeof(int[]), typeof(ReadOnlySpan)), Is.EqualTo(C.None)); + + // Roslyn: CS0266 - only an EXPLICIT (span) conversion exists. In particular the + // user-defined route via Span.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)), 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> { new List { 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, 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))), typeof(Action)); + Assert.That(conversion.IsMethodGroupConversion); + Assert.That(conversion.IsValid); + } + #endregion } } diff --git a/ICSharpCode.Decompiler.Tests/Semantics/ExplicitConversionTest.cs b/ICSharpCode.Decompiler.Tests/Semantics/ExplicitConversionTest.cs index 32808dc46..e23e4d563 100644 --- a/ICSharpCode.Decompiler.Tests/Semantics/ExplicitConversionTest.cs +++ b/ICSharpCode.Decompiler.Tests/Semantics/ExplicitConversionTest.cs @@ -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/ReadOnlySpan when an explicit reference conversion relates the element + // types, and user-defined operators are not considered for such pairs. Roslyn + // compiles '(Span)objectArray', and reports CS0266 (explicit conversion + // exists) for 'Span s = stringArray;'. + var downcast = SpanExplicitConversion(typeof(object[]), typeof(Span)); + Assert.That(downcast.IsValid); + Assert.That(!downcast.IsUserDefined); + + var downcastRos = SpanExplicitConversion(typeof(object[]), typeof(ReadOnlySpan)); + Assert.That(downcastRos.IsValid); + Assert.That(!downcastRos.IsUserDefined); + + var upcast = SpanExplicitConversion(typeof(string[]), typeof(Span)); + Assert.That(upcast.IsValid); + Assert.That(!upcast.IsUserDefined); + } + + [Test] + public void NoExplicitSpanConversionWithoutElementReferenceConversion() + { + // Roslyn: CS0030 - int[] and Span/ReadOnlySpan are unrelated; the + // user-defined operator route (op_Implicit(long[])) must not resurrect the cast. + Assert.That(!SpanExplicitConversion(typeof(int[]), typeof(Span)).IsValid); + Assert.That(!SpanExplicitConversion(typeof(int[]), typeof(ReadOnlySpan)).IsValid); + } + #endregion } } diff --git a/ICSharpCode.Decompiler.Tests/Semantics/OverloadResolutionTests.cs b/ICSharpCode.Decompiler.Tests/Semantics/OverloadResolutionTests.cs index cabe83281..8b2081ad8 100644 --- a/ICSharpCode.Decompiler.Tests/Semantics/OverloadResolutionTests.cs +++ b/ICSharpCode.Decompiler.Tests/Semantics/OverloadResolutionTests.cs @@ -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 Method(a => a.ToString()); } } + + #region First-class span betterness + + public struct ConvertibleToBothReadOnlySpans + { + public static implicit operator ReadOnlySpan(ConvertibleToBothReadOnlySpans c) + { + return default; + } + + public static implicit operator ReadOnlySpan(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 is a better conversion target than + // ReadOnlySpan only if an implicit conversion exists from ReadOnlySpan + // to ReadOnlySpan - the span types, not the element types. No span conversion + // relates ReadOnlySpan and ReadOnlySpan, 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))), Is.EqualTo(OverloadResolutionErrors.None)); + Assert.That(r.AddCandidate(MakeMethodIn(c, typeof(ReadOnlySpan))), 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 { + 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))), + Is.EqualTo(OverloadResolutionErrors.None)); + + var explicitIn = new OverloadResolution(c, new[] { + new ByReferenceResolveResult(arrayArg, ReferenceKind.In) + }); + Assert.That(explicitIn.AddCandidate(MakeInMethodIn(c, typeof(ReadOnlySpan))), + Is.Not.EqualTo(OverloadResolutionErrors.None)); + } + + [Test] + public void InReadOnlySpanParameter_BindsAnIdentityArgumentWithAndWithoutIn() + { + var c = RefAssemblyCompilation.Instance; + var rosArg = new ResolveResult(c.FindType(typeof(ReadOnlySpan))); + + var implicitIn = new OverloadResolution(c, new[] { rosArg }); + Assert.That(implicitIn.AddCandidate(MakeInMethodIn(c, typeof(ReadOnlySpan))), + Is.EqualTo(OverloadResolutionErrors.None)); + + var explicitIn = new OverloadResolution(c, new[] { + new ByReferenceResolveResult(rosArg, ReferenceKind.In) + }); + Assert.That(explicitIn.AddCandidate(MakeInMethodIn(c, typeof(ReadOnlySpan))), + Is.EqualTo(OverloadResolutionErrors.None)); + } + + [Test] + public void ByValueOverloadPreferredOverInOverload_WithoutInAtTheCall() + { + // Roslyn: for F(ReadOnlySpan) vs F(in ReadOnlySpan), 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))), + new ResolveResult(new ArrayType(c, c.FindType(KnownTypeCode.Int32))) + }) + { + var r = new OverloadResolution(c, new[] { arg }); + var byValue = MakeMethodIn(c, typeof(ReadOnlySpan)); + Assert.That(r.AddCandidate(byValue), Is.EqualTo(OverloadResolutionErrors.None)); + Assert.That(r.AddCandidate(MakeInMethodIn(c, typeof(ReadOnlySpan))), + 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' and + // 'M(out arr)' against 'out ReadOnlySpan' - 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), kind)), + Is.Not.EqualTo(OverloadResolutionErrors.None), kind.ToString()); + + var valueArgument = new OverloadResolution(c, new[] { arrayArg }); + Assert.That(valueArgument.AddCandidate(MakeByRefMethodIn(c, typeof(ReadOnlySpan), 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))), ReferenceKind.In) + }); + Assert.That(r.AddCandidate(MakeMethodIn(c, typeof(ReadOnlySpan))), + Is.Not.EqualTo(OverloadResolutionErrors.None)); + var inOverload = MakeInMethodIn(c, typeof(ReadOnlySpan)); + 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 -> ReadOnlySpan + // exists, so ReadOnlySpan 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)); + Assert.That(r.AddCandidate(better), Is.EqualTo(OverloadResolutionErrors.None)); + Assert.That(r.AddCandidate(MakeMethodIn(c, typeof(ReadOnlySpan))), Is.EqualTo(OverloadResolutionErrors.None)); + Assert.That(!r.IsAmbiguous); + Assert.That(r.BestCandidate, Is.SameAs(better)); + } + + #endregion } } diff --git a/ICSharpCode.Decompiler.Tests/Semantics/RefAssemblyCompilation.cs b/ICSharpCode.Decompiler.Tests/Semantics/RefAssemblyCompilation.cs new file mode 100644 index 000000000..6344f6269 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/Semantics/RefAssemblyCompilation.cs @@ -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 +{ + /// + /// 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<T>/ReadOnlySpan<T>. The reference assembly + /// is read once for the whole test run. + /// + static class RefAssemblyCompilation + { + public static ICompilation Instance => instance.Value; + + static readonly Lazy instance = new Lazy( + 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))); + }); + } +} diff --git a/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs b/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs index 2d0dfb4f9..84bb4758a 100644 --- a/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs +++ b/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs @@ -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 tupleCompilation = new Lazy( - 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 // 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(T, T)); @@ -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 { // Signature: M(IList 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 // Signature: M(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 // Invocation: M(listOfAB, actionOfListOfAC); -> T = IList<(int a, string)> // Action 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 // Signature: M(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 // 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 { // Signature: M(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 Is.EqualTo(Resolve(typeof(List), typeof(List), typeof(Collection), typeof(Collection), typeof(ReadOnlyCollection), typeof(ReadOnlyCollection), typeof(System.Runtime.CompilerServices.ReadOnlyCollectionBuilder), typeof(System.Runtime.CompilerServices.ReadOnlyCollectionBuilder)))); } #endregion + + #region First-class span type inference + IType[] InferSpan(Func parameterTypes, + Func 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(Span, T) called with (Span, object): Span is invariant, so the + // span argument contributes an EXACT bound (C# 14 spec, 12.6.3.10: "If V is a + // Span, 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(ReadOnlySpan, T) called with (Span, 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 } } 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 new file mode 100644 index 000000000..f1f0d9024 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanConversions.cs @@ -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(SpanConvertible c) + { + return default(ReadOnlySpan); + } + } + + internal class DerivedSpanConvertible : SpanConvertible + { + } + + public static void AcceptReadOnlySpanChar(ReadOnlySpan s) + { + } + + public static void AcceptReadOnlySpanBase(ReadOnlySpan s) + { + } + + public static void AcceptInReadOnlySpan(in ReadOnlySpan s) + { + } + + public static void ObjectOrReadOnlySpanChar(object a) + { + } + + public static void ObjectOrReadOnlySpanChar(ReadOnlySpan a) + { + } + + public static void StringArgument(string s) + { + AcceptReadOnlySpanChar(s); + ObjectOrReadOnlySpanChar(s); + s.ExtensionOnReadOnlySpanChar(); + } + + public static ReadOnlySpan 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 readOnlySpan = s; + return readOnlySpan.Length + readOnlySpan.Length; + } + + public static void VarianceReadOnlySpan(ReadOnlySpan s) + { + AcceptReadOnlySpanBase(s); + } + + public static void VarianceSpan(Span s) + { + AcceptReadOnlySpanBase(s); + } + + public static ReadOnlySpan VarianceReturn(ReadOnlySpan s) + { + return s; + } + + public static void CovariantArrayToReadOnlySpan(Derived[] a) + { + 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); + } + + public static void ByValueOrIn(ReadOnlySpan s) + { + } + + public static void ByValueOrIn(in ReadOnlySpan s) + { + } + + public static void CallByValueOrIn(ReadOnlySpan 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 s) + { + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanTypes.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanTypes.cs new file mode 100644 index 000000000..5bfdd89ed --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanTypes.cs @@ -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 a) + { + } + + public static void ArrayOrSpan(int[] a) + { + } + + public static void ArrayOrSpan(Span a) + { + } + + public static void SpanOrReadOnlySpan(Span a) + { + } + + public static void SpanOrReadOnlySpan(ReadOnlySpan a) + { + } + + public static void ObjectOrReadOnlySpan(object a) + { + } + + public static void ObjectOrReadOnlySpan(ReadOnlySpan a) + { + } + + public static void ObjectOrReadOnlySpanChar(object a) + { + } + + public static void ObjectOrReadOnlySpanChar(ReadOnlySpan a) + { + } + + public static void EnumerableOrReadOnlySpan(IEnumerable a) + { + } + + public static void EnumerableOrReadOnlySpan(ReadOnlySpan a) + { + } + + public static void CovariantArrayOrReadOnlySpan(object[] a) + { + } + + public static void CovariantArrayOrReadOnlySpan(ReadOnlySpan a) + { + } + + public static void ReadOnlySpanOfObjectOrString(ReadOnlySpan a) + { + } + + public static void ReadOnlySpanOfObjectOrString(ReadOnlySpan a) + { + } + + public static void StringOrReadOnlySpanChar(string a) + { + } + + public static void StringOrReadOnlySpanChar(ReadOnlySpan a) + { + } + + public static void ParamsArrayOrParamsReadOnlySpan(params int[] a) + { + } + + public static void ParamsArrayOrParamsReadOnlySpan(params ReadOnlySpan a) + { + } + + public static void RefSpanOrByValue(ref ReadOnlySpan s) + { + } + + public static void RefSpanOrByValue(ReadOnlySpan s) + { + } + + public static void OutSpanOrByValue(out ReadOnlySpan s) + { + s = default(ReadOnlySpan); + } + + public static void OutSpanOrByValue(ReadOnlySpan s) + { + } + + public static void GenericArrayOrReadOnlySpan(T[] a) + { + } + + public static void GenericArrayOrReadOnlySpan(ReadOnlySpan a) + { + } + + public static void InferFromReadOnlySpan(ReadOnlySpan a) + { + } + + public static ReadOnlySpan ArrayToReadOnlySpanReturn(int[] a) + { + return a; + } + + public static ReadOnlySpan TernaryArrayOrSpan(bool b, int[] a, Span s) + { +#if OPT + if (!b) + { + return s; + } + return a; +#else + return b ? ((ReadOnlySpan)a) : ((ReadOnlySpan)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 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 s = arr; + RefSpanOrByValue(arr); + RefSpanOrByValue(ref s); + OutSpanOrByValue(arr); + OutSpanOrByValue(out s); + } + + public static void CallLosersWithExplicitConversions(int[] arr, string str) + { + ArrayOrReadOnlySpan((ReadOnlySpan)arr); + ArrayOrSpan((Span)arr); + SpanOrReadOnlySpan((Span)arr); + ObjectOrReadOnlySpan((object)arr); + ObjectOrReadOnlySpanChar((object)str); + EnumerableOrReadOnlySpan((IEnumerable)arr); + StringOrReadOnlySpanChar((ReadOnlySpan)str); + } + } + + internal static class FirstClassSpanTypesExtensions + { + public static void ExtensionOnReadOnlySpan(this ReadOnlySpan s) + { + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs index 65136e895..26ab9cc46 100644 --- a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs +++ b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs @@ -601,6 +601,18 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem public void Dispose() { } } + /// + /// 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. + /// + public static class SpanReceiverExtensionTestCase + { + public static void M(this ReadOnlySpan receiver) + { + } + } + /// /// Fixtures for ConversionTest.MethodGroupConversion_* tests: delegate types and /// per-test method sets, resolved through hand-built MethodGroupResolveResults. diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index 62e7d9f66..b56fa2e48 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -329,6 +329,46 @@ 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); + } + + // 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 callArguments, IReadOnlyList? argumentToParameterMap = null, @@ -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: "", Name: "InlineArrayAsSpan" or "InlineArrayAsReadOnlySpan" } && argumentList.Length == 2) @@ -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 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); diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs index 1c12c78a3..200ef491e 100644 --- a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs +++ b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs @@ -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 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 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 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 return false; } + /// + /// C# 14 explicit span conversion: from a single-dimensional array to Span<U> or + /// ReadOnlySpan<U> 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<object>) also lands here. + /// + 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 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 /// 0 = neither is better; 1 = t1 is better; 2 = t2 is better 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 beats Span. This must pre-empt the mutual-convertibility rule + // below, which would conclude the opposite from the Span-to-ReadOnlySpan conversion. + // The ReadOnlySpan-vs-ReadOnlySpan 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; } { diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/OverloadResolution.cs b/ICSharpCode.Decompiler/CSharp/Resolver/OverloadResolution.cs index 53d972467..72da5fa6a 100644 --- a/ICSharpCode.Decompiler/CSharp/Resolver/OverloadResolution.cs +++ b/ICSharpCode.Decompiler/CSharp/Resolver/OverloadResolution.cs @@ -206,6 +206,13 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver /// public bool AllowImplicitIn { get; set; } = true; + /// + /// 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. + /// + public bool AllowSpanConversionOnExtensionReceiver { get; set; } = true; + /// /// Gets/Sets whether ConversionResolveResults created by this OverloadResolution /// instance apply overflow checking. @@ -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 { diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs b/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs index d579cfbdb..0e07ca7ba 100644 --- a/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs +++ b/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs @@ -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 is invariant, so even in a lower-bound context a Span 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]); 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); } diff --git a/ICSharpCode.Decompiler/Semantics/Conversion.cs b/ICSharpCode.Decompiler/Semantics/Conversion.cs index fe94ce022..f4787b13a 100644 --- a/ICSharpCode.Decompiler/Semantics/Conversion.cs +++ b/ICSharpCode.Decompiler/Semantics/Conversion.cs @@ -97,6 +97,13 @@ namespace ICSharpCode.Decompiler.Semantics /// public static readonly Conversion ImplicitSpanConversion = new BuiltinConversion(true, 13); + /// + /// C# 14 explicit span conversion: from an array type to or + /// where the element types are related by an + /// explicit reference conversion. + /// + 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 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 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 /// public virtual bool IsImplicitSpanConversion => false; + /// + /// Gets whether this is an explicit span conversion from an array type to + /// or whose element types are related by an explicit + /// reference conversion. + /// + public virtual bool IsExplicitSpanConversion => false; + /// /// For a tuple conversion, gets the individual tuple element conversions. /// diff --git a/ICSharpCode.Decompiler/Semantics/MethodGroupResolveResult.cs b/ICSharpCode.Decompiler/Semantics/MethodGroupResolveResult.cs index e999ce6f2..167bac22c 100644 --- a/ICSharpCode.Decompiler/Semantics/MethodGroupResolveResult.cs +++ b/ICSharpCode.Decompiler/Semantics/MethodGroupResolveResult.cs @@ -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 extOr.IsExtensionMethodInvocation = true; extOr.CheckForOverflow = checkForOverflow; extOr.AllowImplicitIn = allowImplicitIn; + extOr.AllowSpanConversionOnExtensionReceiver = allowSpanConversionOnExtensionReceiver; foreach (var g in extensionMethods) {