Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
79 changes: 79 additions & 0 deletions ICSharpCode.Decompiler.Tests/Semantics/ConversionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>, so the span tests resolve against a
// .NET ref assembly; the test assembly provides the extension-method fixture.
static readonly Lazy<ICompilation> spanCompilation = new Lazy<ICompilation>(
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<char>)), Is.EqualTo(C.ImplicitSpanConversion),
"the span conversion must win over String's own op_Implicit: user-defined conversions are not considered between span-convertible types");
Assert.That(SpanConversion(typeof(string[]), typeof(Span<string>)), Is.EqualTo(C.ImplicitSpanConversion));
Assert.That(SpanConversion(typeof(string[]), typeof(ReadOnlySpan<object>)), Is.EqualTo(C.ImplicitSpanConversion));
Assert.That(SpanConversion(typeof(Span<string>), typeof(ReadOnlySpan<object>)), Is.EqualTo(C.ImplicitSpanConversion));
Assert.That(SpanConversion(typeof(ReadOnlySpan<string>), typeof(ReadOnlySpan<object>)), Is.EqualTo(C.ImplicitSpanConversion));
}

[Test]
public void NoImplicitSpanConversionWithoutElementCovariance()
{
// Roslyn: CS0029 - no conversion at all relates these.
Assert.That(SpanConversion(typeof(int[]), typeof(ReadOnlySpan<long>)), Is.EqualTo(C.None));

// Roslyn: CS0266 - only an EXPLICIT (span) conversion exists. In particular the
// user-defined route via Span<object>.op_Implicit(object[]) plus array covariance
// must not be considered, because a span conversion exists for the pair.
Assert.That(SpanConversion(typeof(string[]), typeof(Span<object>)), Is.EqualTo(C.None));
}

Conversion SpanMethodGroupConversion(ResolveResult target, Type delegateType)
{
var c = 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<List<IMethod>> { new List<IMethod> { extensionMethod } };
return CSharpConversions.Get(c).ImplicitConversion(mgrr, c.FindType(delegateType));
}

[Test]
public void MethodGroupConversion_SpanConversionOnTheReceiverIsNotConsidered()
{
// C# 14 first-class spans: "span conversion is not considered when overload
// resolution is performed for a method group conversion". For 'str.M' with M being
// an extension on ReadOnlySpan<char>, Roslyn reports CS0123, even though the
// invocation 'str.M()' is legal.
var c = 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<char>))), typeof(Action));
Assert.That(conversion.IsMethodGroupConversion);
Assert.That(conversion.IsValid);
}
#endregion
}
}
48 changes: 48 additions & 0 deletions ICSharpCode.Decompiler.Tests/Semantics/ExplicitConversionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>, so the span tests resolve against a
// .NET ref assembly.
static readonly Lazy<ICompilation> spanCompilation = new Lazy<ICompilation>(
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<U>/ReadOnlySpan<U> when an explicit reference conversion relates the element
// types, and user-defined operators are not considered for such pairs. Roslyn
// compiles '(Span<string>)objectArray', and reports CS0266 (explicit conversion
// exists) for 'Span<object> s = stringArray;'.
var downcast = SpanExplicitConversion(typeof(object[]), typeof(Span<string>));
Assert.That(downcast.IsValid);
Assert.That(!downcast.IsUserDefined);

var downcastRos = SpanExplicitConversion(typeof(object[]), typeof(ReadOnlySpan<string>));
Assert.That(downcastRos.IsValid);
Assert.That(!downcastRos.IsUserDefined);

var upcast = SpanExplicitConversion(typeof(string[]), typeof(Span<object>));
Assert.That(upcast.IsValid);
Assert.That(!upcast.IsUserDefined);
}

[Test]
public void NoExplicitSpanConversionWithoutElementReferenceConversion()
{
// Roslyn: CS0030 - int[] and Span<long>/ReadOnlySpan<long> are unrelated; the
// user-defined operator route (op_Implicit(long[])) must not resurrect the cast.
Assert.That(!SpanExplicitConversion(typeof(int[]), typeof(Span<long>)).IsValid);
Assert.That(!SpanExplicitConversion(typeof(int[]), typeof(ReadOnlySpan<long>)).IsValid);
}
#endregion
}
}
185 changes: 185 additions & 0 deletions ICSharpCode.Decompiler.Tests/Semantics/OverloadResolutionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<int>(ConvertibleToBothReadOnlySpans c)
{
return default;
}

public static implicit operator ReadOnlySpan<long>(ConvertibleToBothReadOnlySpans c)
{
return default;
}
}

// The legacy reference mscorlib predates Span<T>, so span tests resolve against a
// .NET ref assembly; the test assembly itself provides the operator fixture above.
static readonly Lazy<ICompilation> spanCompilation = new Lazy<ICompilation>(
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<E1> is a better conversion target than
// ReadOnlySpan<E2> only if an implicit conversion exists from ReadOnlySpan<E1>
// to ReadOnlySpan<E2> - the span types, not the element types. No span conversion
// relates ReadOnlySpan<int> and ReadOnlySpan<long>, so neither target is better
// and the call is ambiguous; Roslyn reports CS0121.
var c = spanCompilation.Value;
var r = new OverloadResolution(c, new[] {
new ResolveResult(c.FindType(typeof(ConvertibleToBothReadOnlySpans)))
});
Assert.That(r.AddCandidate(MakeMethodIn(c, typeof(ReadOnlySpan<int>))), Is.EqualTo(OverloadResolutionErrors.None));
Assert.That(r.AddCandidate(MakeMethodIn(c, typeof(ReadOnlySpan<long>))), Is.EqualTo(OverloadResolutionErrors.None));
Assert.That(r.IsAmbiguous);
}

static IMethod MakeByRefMethodIn(ICompilation c, Type parameterType, ReferenceKind kind)
{
var m = new FakeMethod(c, SymbolKind.Method);
m.Name = "Method";
m.Parameters = new List<IParameter> {
new DefaultParameter(new ByReferenceType(c.FindType(parameterType)), string.Empty,
owner: m, referenceKind: kind)
};
return m;
}

static IMethod MakeInMethodIn(ICompilation c, Type parameterType)
=> MakeByRefMethodIn(c, parameterType, ReferenceKind.In);

[Test]
public void InReadOnlySpanParameter_BindsAnArrayWithoutInButNotWithIn()
{
// Roslyn: OnlyIn(arr) compiles - a value argument may bind to an 'in' parameter
// through the implicit span conversion (a temporary is created). OnlyIn(in arr)
// is CS1503: an explicit 'in' argument must have the parameter's own type.
var c = 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<int>))),
Is.EqualTo(OverloadResolutionErrors.None));

var explicitIn = new OverloadResolution(c, new[] {
new ByReferenceResolveResult(arrayArg, ReferenceKind.In)
});
Assert.That(explicitIn.AddCandidate(MakeInMethodIn(c, typeof(ReadOnlySpan<int>))),
Is.Not.EqualTo(OverloadResolutionErrors.None));
}

[Test]
public void InReadOnlySpanParameter_BindsAnIdentityArgumentWithAndWithoutIn()
{
var c = spanCompilation.Value;
var rosArg = new ResolveResult(c.FindType(typeof(ReadOnlySpan<int>)));

var implicitIn = new OverloadResolution(c, new[] { rosArg });
Assert.That(implicitIn.AddCandidate(MakeInMethodIn(c, typeof(ReadOnlySpan<int>))),
Is.EqualTo(OverloadResolutionErrors.None));

var explicitIn = new OverloadResolution(c, new[] {
new ByReferenceResolveResult(rosArg, ReferenceKind.In)
});
Assert.That(explicitIn.AddCandidate(MakeInMethodIn(c, typeof(ReadOnlySpan<int>))),
Is.EqualTo(OverloadResolutionErrors.None));
}

[Test]
public void ByValueOverloadPreferredOverInOverload_WithoutInAtTheCall()
{
// Roslyn: for F(ReadOnlySpan<int>) vs F(in ReadOnlySpan<int>), a call without 'in'
// picks the by-value overload - both for an identity argument and through the
// span conversion from int[].
var c = spanCompilation.Value;
foreach (var arg in new[] {
new ResolveResult(c.FindType(typeof(ReadOnlySpan<int>))),
new ResolveResult(new ArrayType(c, c.FindType(KnownTypeCode.Int32)))
})
{
var r = new OverloadResolution(c, new[] { arg });
var byValue = MakeMethodIn(c, typeof(ReadOnlySpan<int>));
Assert.That(r.AddCandidate(byValue), Is.EqualTo(OverloadResolutionErrors.None));
Assert.That(r.AddCandidate(MakeInMethodIn(c, typeof(ReadOnlySpan<int>))),
Is.EqualTo(OverloadResolutionErrors.None));
Assert.That(!r.IsAmbiguous, $"argument {arg.Type}");
Assert.That(r.BestCandidate, Is.SameAs(byValue), $"argument {arg.Type}");
}
}

[Test]
public void RefAndOutParametersNeverBindThroughASpanConversion()
{
// Roslyn: CS1503 for both 'M(ref arr)' against 'ref ReadOnlySpan<int>' and
// 'M(out arr)' against 'out ReadOnlySpan<int>' - ref and out demand the
// parameter's own type; the span conversion does not apply. A value argument
// without the keyword is a passing-mode mismatch regardless of conversions.
var c = 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<int>), kind)),
Is.Not.EqualTo(OverloadResolutionErrors.None), kind.ToString());

var valueArgument = new OverloadResolution(c, new[] { arrayArg });
Assert.That(valueArgument.AddCandidate(MakeByRefMethodIn(c, typeof(ReadOnlySpan<int>), kind)),
Is.Not.EqualTo(OverloadResolutionErrors.None), kind.ToString());
}
}

[Test]
public void InOverloadIsTheOnlyCandidateWithInAtTheCall()
{
// Roslyn: F(in ros) picks the in-overload; the by-value overload cannot take an
// 'in' argument.
var c = spanCompilation.Value;
var r = new OverloadResolution(c, new[] {
new ByReferenceResolveResult(new ResolveResult(c.FindType(typeof(ReadOnlySpan<int>))), ReferenceKind.In)
});
Assert.That(r.AddCandidate(MakeMethodIn(c, typeof(ReadOnlySpan<int>))),
Is.Not.EqualTo(OverloadResolutionErrors.None));
var inOverload = MakeInMethodIn(c, typeof(ReadOnlySpan<int>));
Assert.That(r.AddCandidate(inOverload), Is.EqualTo(OverloadResolutionErrors.None));
Assert.That(r.BestCandidate, Is.SameAs(inOverload));
}

[Test]
public void ReadOnlySpanOfStringPreferredOverReadOnlySpanOfObject()
{
// The positive direction of the same rule: string[] converts to both targets, and
// the covariant span conversion ReadOnlySpan<string> -> ReadOnlySpan<object>
// exists, so ReadOnlySpan<string> is the better target.
var c = spanCompilation.Value;
var r = new OverloadResolution(c, new[] {
new ResolveResult(new ArrayType(c, c.FindType(KnownTypeCode.String)))
});
var better = MakeMethodIn(c, typeof(ReadOnlySpan<string>));
Assert.That(r.AddCandidate(better), Is.EqualTo(OverloadResolutionErrors.None));
Assert.That(r.AddCandidate(MakeMethodIn(c, typeof(ReadOnlySpan<object>))), Is.EqualTo(OverloadResolutionErrors.None));
Assert.That(!r.IsAmbiguous);
Assert.That(r.BestCandidate, Is.SameAs(better));
}

#endregion
}
}
Loading
Loading