diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 5bb191410e..29755b3d4d 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -794,6 +794,18 @@ public async Task RefStructInterfaces([ValueSource(nameof(roslyn4OrNewerOptions) 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 7a9b9f51fd..640a6587cc 100644 --- a/ICSharpCode.Decompiler.Tests/Semantics/ConversionTests.cs +++ b/ICSharpCode.Decompiler.Tests/Semantics/ConversionTests.cs @@ -1764,5 +1764,84 @@ public override Conversion IsValid(IType[] parameterTypes, IType returnType, CSh return conversions.ImplicitConversion(bodyReturnType, returnType); } } + + #region First-class span conversions + // The legacy reference mscorlib predates Span, so the span tests resolve against a + // .NET ref assembly; the test assembly provides the extension-method fixture. + static readonly Lazy spanCompilation = new Lazy( + delegate { + string path = System.IO.Path.Combine( + Helpers.Tester.RefAssembliesToolset.GetPath(".NETCoreApp,Version=v5.0"), "System.Runtime.dll"); + return new SimpleCompilation(TypeSystemLoaderTests.TestAssembly, + new Decompiler.Metadata.PEFile(path, new System.IO.FileStream(path, System.IO.FileMode.Open, System.IO.FileAccess.Read))); + }); + + Conversion SpanConversion(Type from, Type to) + { + var c = spanCompilation.Value; + 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 = spanCompilation.Value; + 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 = spanCompilation.Value; + 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 = spanCompilation.Value; + 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 32808dc465..9b830f9232 100644 --- a/ICSharpCode.Decompiler.Tests/Semantics/ExplicitConversionTest.cs +++ b/ICSharpCode.Decompiler.Tests/Semantics/ExplicitConversionTest.cs @@ -766,5 +766,53 @@ public void ExplicitTypeParameterConversionFromEffectiveBaseClass() 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 + // The legacy reference mscorlib predates Span, so the span tests resolve against a + // .NET ref assembly. + static readonly Lazy spanCompilation = new Lazy( + delegate { + string path = System.IO.Path.Combine( + Helpers.Tester.RefAssembliesToolset.GetPath(".NETCoreApp,Version=v5.0"), "System.Runtime.dll"); + return new SimpleCompilation( + new Decompiler.Metadata.PEFile(path, new System.IO.FileStream(path, System.IO.FileMode.Open, System.IO.FileAccess.Read))); + }); + + Conversion SpanExplicitConversion(Type from, Type to) + { + var c = spanCompilation.Value; + 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 cabe83281b..4d72b7cdf6 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,188 @@ public static void Main(string[] args) 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; + } + } + + // The legacy reference mscorlib predates Span, so span tests resolve against a + // .NET ref assembly; the test assembly itself provides the operator fixture above. + static readonly Lazy spanCompilation = new Lazy( + delegate { + string path = Path.Combine( + Helpers.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))); + }); + + 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 = spanCompilation.Value; + 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 = spanCompilation.Value; + 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 = spanCompilation.Value; + 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 = spanCompilation.Value; + 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 = spanCompilation.Value; + 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 = spanCompilation.Value; + 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 = spanCompilation.Value; + 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/TypeInferenceTests.cs b/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs index c02067d989..470947d694 100644 --- a/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs +++ b/ICSharpCode.Decompiler.Tests/Semantics/TypeInferenceTests.cs @@ -1160,5 +1160,112 @@ public void CommonSubTypeIEnumerableClonableIEnumerableComparableList() 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 + // The legacy reference mscorlib predates Span, so these tests resolve against a + // .NET ref assembly, like the tuple tests above. + static readonly Lazy spanCompilation = 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))); + }); + + IType[] InferSpan(Func parameterTypes, + Func arguments, out bool success) + { + var c = spanCompilation.Value; + 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[] { spanCompilation.Value.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[] { spanCompilation.Value.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[] { spanCompilation.Value.FindType(KnownTypeCode.Object) })); + Assert.That(success); + } + #endregion } } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanConversions.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanConversions.cs new file mode 100644 index 0000000000..b018fa9c9d --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FirstClassSpanConversions.cs @@ -0,0 +1,123 @@ +// 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 + { + } + + 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 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 0000000000..5bfdd89ed1 --- /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 14941b5857..f69b0a17c4 100644 --- a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs +++ b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs @@ -572,6 +572,18 @@ public class ClassImplementingIDisposable : IDisposable 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 1efe31af69..0e480e845e 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -329,6 +329,30 @@ internal static bool IsStringToReadOnlySpanCharImplicitConversion(IMethod method && method.Parameters[0].Type.IsKnownType(KnownTypeCode.String); } + /// + /// Matches MemoryExtensions.AsSpan(string), the helper the C# 14 compiler emits for the + /// implicit span conversion from string to ReadOnlySpan<char>. + /// + internal static bool IsStringToReadOnlySpanCharAsSpan(IMethod method) + { + return method is { IsStatic: true, Name: "AsSpan", Parameters.Count: 1, TypeArguments.Count: 0 } + && method.DeclaringType.FullName == "System.MemoryExtensions" + && method.ReturnType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT) + && method.ReturnType.TypeArguments[0].IsKnownType(KnownTypeCode.Char) + && method.Parameters[0].Type.IsKnownType(KnownTypeCode.String); + } + + /// + /// Matches ReadOnlySpan<To>.CastUp<From>(ReadOnlySpan<From>), the helper the + /// C# 14 compiler emits for the covariant implicit span conversion. + /// + internal static bool IsReadOnlySpanCastUp(IMethod method) + { + return method is { IsStatic: true, Name: "CastUp", Parameters.Count: 1, TypeArguments.Count: 1 } + && method.DeclaringType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT) + && method.Parameters[0].Type.IsKnownType(KnownTypeCode.ReadOnlySpanOfT); + } + public ExpressionWithResolveResult Build(OpCode callOpCode, IMethod method, IReadOnlyList callArguments, IReadOnlyList? argumentToParameterMap = null, @@ -481,6 +505,21 @@ public ExpressionWithResolveResult Build(OpCode callOpCode, IMethod method, return HandleImplicitConversion(method, argumentList.Arguments[0]); } + if (settings.FirstClassSpanTypes && argumentList.Length == 1 + && (IsStringToReadOnlySpanCharAsSpan(method) || IsReadOnlySpanCastUp(method))) + { + // The C# 14 compiler emits these helpers for implicit span conversions; fold the + // call back into the conversion. Only safe when the conversion actually applies + // to this argument type - otherwise keep the call (e.g. AsSpan on a null literal). + var spanConv = CSharpConversions.Get(expressionBuilder.compilation) + .ImplicitConversion(argumentList.Arguments[0].Type, method.ReturnType); + if (spanConv.IsImplicitSpanConversion) + { + argumentList.CheckNoNamedOrOptionalArguments(); + return HandleImplicitConversion(method, argumentList.Arguments[0]); + } + } + if (settings.InlineArrays && method is { DeclaringType.FullName: "", Name: "InlineArrayAsSpan" or "InlineArrayAsReadOnlySpan" } && argumentList.Length == 2) @@ -1024,6 +1063,15 @@ private ArgumentList BuildArgumentList(ExpectedTargetDetails expectedTargetDetai if (parameter.ReferenceKind != ReferenceKind.None) { arg = ExpressionBuilder.ChangeDirectionExpressionTo(arg, parameter.ReferenceKind, callArguments[i] is AddressOf); + // An rvalue bound to an 'in' parameter loses its DirectionExpression above and + // is an ordinary value expression: give a span conversion the same chance to + // become implicit that by-value arguments get from the ConvertTo call above. + if (arg.Expression is not DirectionExpression + && parameter.Type.SkipModifiers() is ByReferenceType brt + && arg.ResolveResult is ConversionResolveResult { Conversion.IsImplicitSpanConversion: true }) + { + arg = arg.ConvertTo(brt.ElementType, expressionBuilder, allowImplicitConversion: true); + } } arguments.Add(arg); @@ -1535,7 +1583,12 @@ private ExpressionWithResolveResult HandleImplicitConversion(IMethod method, Tra 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))) + // An implicit span conversion subsumes the conversion helper the compiler emitted: + // the language defines the conversion in terms of exactly these helpers. + bool directlyConvertible = conv.IsValid + && (conv.IsImplicitSpanConversion + || (conv.IsUserDefined && conv.Method.Equals(method, NormalizeTypeVisitor.TypeErasure))); + 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 044c61487c..55d8cc4b13 100644 --- a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs +++ b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs @@ -349,6 +349,8 @@ Conversion ExplicitConversionImpl(IType fromType, IType toType) 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); @@ -1037,6 +1039,15 @@ Conversion UserDefinedImplicitConversion(ResolveResult fromResult, IType fromTyp 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) @@ -1086,6 +1097,13 @@ Conversion UserDefinedExplicitConversion(ResolveResult fromResult, IType fromTyp 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) { @@ -1274,6 +1292,31 @@ bool IsImplicitSpanConversion(IType fromType, IType toType) 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 @@ -1393,7 +1436,10 @@ Conversion MethodGroupConversion(ResolveResult resolveResult, IType toType) 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) { @@ -1679,36 +1725,21 @@ public int BetterConversion(IType s, IType t1, IType t2) /// 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/MethodGroupResolveResult.cs b/ICSharpCode.Decompiler/CSharp/Resolver/MethodGroupResolveResult.cs index 07155069a4..6ac08d4df3 100644 --- a/ICSharpCode.Decompiler/CSharp/Resolver/MethodGroupResolveResult.cs +++ b/ICSharpCode.Decompiler/CSharp/Resolver/MethodGroupResolveResult.cs @@ -250,7 +250,8 @@ public OverloadResolution PerformOverloadResolution(ICompilation compilation, Re 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); @@ -288,6 +289,7 @@ public OverloadResolution PerformOverloadResolution(ICompilation compilation, Re extOr.IsExtensionMethodInvocation = true; extOr.CheckForOverflow = checkForOverflow; extOr.AllowImplicitIn = allowImplicitIn; + extOr.AllowSpanConversionOnExtensionReceiver = allowSpanConversionOnExtensionReceiver; foreach (var g in extensionMethods) { diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/OverloadResolution.cs b/ICSharpCode.Decompiler/CSharp/Resolver/OverloadResolution.cs index 8e77d3133f..a2e6769875 100644 --- a/ICSharpCode.Decompiler/CSharp/Resolver/OverloadResolution.cs +++ b/ICSharpCode.Decompiler/CSharp/Resolver/OverloadResolution.cs @@ -206,6 +206,13 @@ public OverloadResolution(ICompilation compilation, ResolveResult[] arguments, s /// 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 @@ void CheckApplicability(Candidate candidate) 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 f90d45200e..3965bf7b64 100644 --- a/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs +++ b/ICSharpCode.Decompiler/CSharp/Resolver/TypeInference.cs @@ -771,11 +771,13 @@ void MakeLowerBoundInference(IType U, IType V) 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 efbf1ee693..1a9ca1a854 100644 --- a/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs +++ b/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs @@ -652,6 +652,13 @@ bool CastCanBeMadeImplicit(Resolver.CSharpConversions conversions, Conversion co 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 fe94ce0223..f4787b13a4 100644 --- a/ICSharpCode.Decompiler/Semantics/Conversion.cs +++ b/ICSharpCode.Decompiler/Semantics/Conversion.cs @@ -97,6 +97,13 @@ public static Conversion EnumerationConversion(bool isImplicit, bool isLifted) /// 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 @@ public override bool IsThrowExpressionConversion { 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 @@ public override string ToString() 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 @@ public virtual IMethod Method { /// 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. ///