Skip to content

Commit

Permalink
Test and document Span breaking changes (#75079)
Browse files Browse the repository at this point in the history
* Test more breaking changes

* Document the main breaking changes

* Improve the doc
  • Loading branch information
jjonescz authored Sep 16, 2024
1 parent badc740 commit 4d28028
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 11 deletions.
43 changes: 43 additions & 0 deletions docs/compilers/CSharp/Compiler Breaking Changes - DotNet 10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# This document lists known breaking changes in Roslyn after .NET 9 all the way to .NET 10.

## `Span<T>` and `ReadOnlySpan<T>` overloads are applicable in more scenarios in C# 14 and newer

***Introduced in Visual Studio 2022 version 17.13***

C# 14 introduces new [built-in span conversions and type inference rules](https://github.com/dotnet/csharplang/issues/7905).
This means that different overloads might be chosen compared to C# 13, and sometimes an ambiguity compile-time error
might be raised because a new overload is applicable but there is no single best overload.

The following example shows some ambiguities and possible workarounds.
Note that another workaround is for API authors to use the `OverloadResolutionPriorityAttribute`.

```cs
var x = new long[] { 1 };
Assert.Equal([2], x); // previously Assert.Equal<T>(T[], T[]), now ambiguous with Assert.Equal<T>(ReadOnlySpan<T>, Span<T>)
Assert.Equal([2], x.AsSpan()); // workaround
var y = new int[] { 1, 2 };
var s = new ArraySegment<int>(x, 1, 1);
Assert.Equal(y, s); // previously Assert.Equal<T>(T, T), now ambiguous with Assert.Equal<T>(Span<T>, Span<T>)
Assert.Equal(y.AsSpan(), s); // workaround
```

A `Span<T>` overload might be chosen in C# 14 where an overload taking an interface implemented by `T[]` (such as `IEnumerable<T>`) was chosen in C# 13,
and that can lead to an `ArrayTypeMismatchException` at runtime if used with a covariant array:

```cs
string[] s = new[] { "a" };
object[] o = s; // array variance
C.R(o); // wrote 1 previously, now crashes in Span<T> constructor with ArrayTypeMismatchException
C.R(o.AsEnumerable()); // workaround
static class C
{
public static void R<T>(IEnumerable<T> e) => Console.Write(1);
public static void R<T>(Span<T> s) => Console.Write(2);
// another workaround:
[OverloadResolutionPriority(1)]
public static void R<T>(ReadOnlySpan<T> s) => Console.Write(3);
}
```
191 changes: 180 additions & 11 deletions src/Compilers/CSharp/Test/Emit3/FirstClassSpanTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -323,22 +323,20 @@ public void BreakingChange_TypeInference_SpanVsIEnumerable_01_Workaround(
using System;
using System.Collections.Generic;
class C
{
void M(int[] a)
{
foreach (var x in a.R()) { }
}
}
int[] a = new int[0];
{{type}}<int> s = default;
a.R();
s.R();
static class E
{
public static void R<T>(this {{type}}<T> s) => throw null;
public static IEnumerable<T> R<T>(this IEnumerable<T> e) => throw null;
public static IEnumerable<T> R<T>(this T[] a) => throw null;
public static void R<T>(this {{type}}<T> s) => Console.Write(1);
public static IEnumerable<T> R<T>(this IEnumerable<T> e) { Console.Write(2); return e; }
public static IEnumerable<T> R<T>(this T[] a) { Console.Write(3); return a; }
}
""";
CreateCompilationWithSpanAndMemoryExtensions(source, parseOptions: TestOptions.Regular.WithLanguageVersion(langVersion)).VerifyDiagnostics();
var comp = CreateCompilationWithSpanAndMemoryExtensions(source, parseOptions: TestOptions.Regular.WithLanguageVersion(langVersion));
CompileAndVerify(comp, expectedOutput: "31").VerifyDiagnostics();
}

[Fact]
Expand Down Expand Up @@ -371,6 +369,64 @@ static class C
CompileAndVerify(comp, expectedOutput: expectedOutput).VerifyDiagnostics();
}

[Theory, MemberData(nameof(LangVersions))]
public void BreakingChange_TypeInference_SpanVsIEnumerable_02_Workaround_AsEnumerable(LanguageVersion langVersion)
{
var source = """
using System;
using System.Collections.Generic;
using System.Linq;
string[] s = new[] { "a" };
object[] o = s;
C.R(o.AsEnumerable());
static class C
{
public static void R<T>(IEnumerable<T> e) => Console.Write(1);
public static void R<T>(Span<T> s) => Console.Write(2);
}
""";
var comp = CreateCompilationWithSpanAndMemoryExtensions(source, parseOptions: TestOptions.Regular.WithLanguageVersion(langVersion));
CompileAndVerify(comp, expectedOutput: "1").VerifyDiagnostics();
}

[Fact]
public void BreakingChange_TypeInference_SpanVsIEnumerable_02_Workaround_OverloadResolutionPriority()
{
var source = """
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
string[] s = new[] { "a" };
object[] o = s;
C.R(o);
static class C
{
public static void R<T>(IEnumerable<T> e) => Console.Write(1);
public static void R<T>(Span<T> s) => Console.Write(2);
[OverloadResolutionPriority(1)]
public static void R<T>(ReadOnlySpan<T> s) => Console.Write(3);
}
""";
var comp = CreateCompilationWithSpanAndMemoryExtensions([source, OverloadResolutionPriorityAttributeDefinition],
parseOptions: TestOptions.Regular13);
CompileAndVerify(comp, expectedOutput: "1").VerifyDiagnostics();

var expectedOutput = "3";

comp = CreateCompilationWithSpanAndMemoryExtensions([source, OverloadResolutionPriorityAttributeDefinition],
parseOptions: TestOptions.RegularNext);
CompileAndVerify(comp, expectedOutput: expectedOutput).VerifyDiagnostics();

comp = CreateCompilationWithSpanAndMemoryExtensions([source, OverloadResolutionPriorityAttributeDefinition]);
CompileAndVerify(comp, expectedOutput: expectedOutput).VerifyDiagnostics();
}

[Fact]
public void BreakingChange_TypeInference_SpanVsIEnumerable_02_ReadOnlySpan()
{
Expand Down Expand Up @@ -461,6 +517,119 @@ static class E
CompileAndVerify(comp, expectedOutput: expectedOutput).VerifyDiagnostics();
}

[Fact]
public void BreakingChange_Conversion_SpanVsArray()
{
var source = """
using System;
var x = new long[] { 1 };
C.M([2], x);
static class C
{
public static void M<T>(T[] a, T[] b) => Console.Write("1 " + typeof(T).Name);
public static void M<T>(ReadOnlySpan<T> a, Span<T> b) => Console.Write("2 " + typeof(T).Name);
}
""";
var comp = CreateCompilationWithSpanAndMemoryExtensions(source, parseOptions: TestOptions.Regular13);
CompileAndVerify(comp, expectedOutput: "1 Int64").VerifyDiagnostics();

var expectedDiagnostics = new[]
{
// (5,3): error CS0121: The call is ambiguous between the following methods or properties: 'C.M<T>(T[], T[])' and 'C.M<T>(ReadOnlySpan<T>, Span<T>)'
// C.M([2], x);
Diagnostic(ErrorCode.ERR_AmbigCall, "M").WithArguments("C.M<T>(T[], T[])", "C.M<T>(System.ReadOnlySpan<T>, System.Span<T>)").WithLocation(5, 3)
};

CreateCompilationWithSpanAndMemoryExtensions(source, parseOptions: TestOptions.RegularNext).VerifyDiagnostics(expectedDiagnostics);
CreateCompilationWithSpanAndMemoryExtensions(source).VerifyDiagnostics(expectedDiagnostics);
}

[Theory, MemberData(nameof(LangVersions))]
public void BreakingChange_Conversion_SpanVsArray_Workaround_AsSpan(LanguageVersion langVersion)
{
var source = """
using System;
var x = new long[] { 1 };
C.M([2], x.AsSpan());
static class C
{
public static void M<T>(T[] a, T[] b) => Console.Write("1 " + typeof(T).Name);
public static void M<T>(ReadOnlySpan<T> a, Span<T> b) => Console.Write("2 " + typeof(T).Name);
}
""";
var comp = CreateCompilationWithSpanAndMemoryExtensions(source, parseOptions: TestOptions.Regular.WithLanguageVersion(langVersion));
CompileAndVerify(comp, expectedOutput: "2 Int64").VerifyDiagnostics();
}

[Fact]
public void BreakingChange_Conversion_SpanVsArray_Workaround_OverloadResolutionPriority()
{
var source = """
using System;
using System.Runtime.CompilerServices;
var x = new long[] { 1 };
C.M([2], x);
static class C
{
public static void M<T>(T[] a, T[] b) => Console.Write("1 " + typeof(T).Name);
[OverloadResolutionPriority(1)]
public static void M<T>(ReadOnlySpan<T> a, Span<T> b) => Console.Write("2 " + typeof(T).Name);
}
""";
var comp = CreateCompilationWithSpanAndMemoryExtensions([source, OverloadResolutionPriorityAttributeDefinition],
parseOptions: TestOptions.Regular13);
CompileAndVerify(comp, expectedOutput: "1 Int64").VerifyDiagnostics();

var expectedOutput = "2 Int64";

comp = CreateCompilationWithSpanAndMemoryExtensions([source, OverloadResolutionPriorityAttributeDefinition],
parseOptions: TestOptions.RegularNext);
CompileAndVerify(comp, expectedOutput: expectedOutput).VerifyDiagnostics();

comp = CreateCompilationWithSpanAndMemoryExtensions([source, OverloadResolutionPriorityAttributeDefinition]);
CompileAndVerify(comp, expectedOutput: expectedOutput).VerifyDiagnostics();
}

[ConditionalFact(typeof(CoreClrOnly))]
public void BreakingChange_Conversion_ArrayVsArraySegment()
{
var source = """
using System;
var x = new int[] { 1, 2 };
var s = new ArraySegment<int>(x, 1, 1);
C.M(x, s);
static class C
{
public static void M<T>(T a, T b) => Console.Write("1 " + typeof(T));
public static void M<T>(Span<T> a, Span<T> b) => Console.Write("2 " + typeof(T).Name);
}
""";
var comp = CreateCompilationWithSpanAndMemoryExtensions(source, parseOptions: TestOptions.Regular13);
CompileAndVerify(comp, expectedOutput: "1 System.ArraySegment`1[System.Int32]").VerifyDiagnostics();

var expectedDiagnostics = new[]
{
// (6,3): error CS0121: The call is ambiguous between the following methods or properties: 'C.M<T>(T, T)' and 'C.M<T>(Span<T>, Span<T>)'
// C.M(x, s);
Diagnostic(ErrorCode.ERR_AmbigCall, "M").WithArguments("C.M<T>(T, T)", "C.M<T>(System.Span<T>, System.Span<T>)").WithLocation(6, 3)
};

CreateCompilationWithSpanAndMemoryExtensions(source, parseOptions: TestOptions.RegularNext).VerifyDiagnostics(expectedDiagnostics);
CreateCompilationWithSpanAndMemoryExtensions(source).VerifyDiagnostics(expectedDiagnostics);
}

[Theory, CombinatorialData]
public void Conversion_Array_Span_Implicit(
[CombinatorialLangVersions] LanguageVersion langVersion,
Expand Down
2 changes: 2 additions & 0 deletions src/Compilers/Test/Utilities/CSharp/TestSources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,8 @@ public static bool SequenceEqual<T> (this Span<T> span, ReadOnlySpan<T> other) w
}
public static ReadOnlySpan<char> AsSpan(this string text) => string.IsNullOrEmpty(text) ? default : new ReadOnlySpan<char>(text.ToCharArray());
public static Span<T> AsSpan<T>(this T[] array) => new Span<T>(array);
}
}";
}
Expand Down

0 comments on commit 4d28028

Please sign in to comment.