Skip to content
Merged
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
61 changes: 61 additions & 0 deletions docs/compilers/CSharp/Compiler Breaking Changes - DotNet 11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Breaking changes in Roslyn after .NET 10.0.100 through .NET 11.0.100

This document lists known breaking changes in Roslyn after .NET 10 general release (.NET SDK version 10.0.100) through .NET 11 general release (.NET SDK version 11.0.100).

## The *safe-context* of a collection expression of Span/ReadOnlySpan type is now *declaration-block*

***Introduced in Visual Studio 2026 version 18.3***
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This version is not out yet, but, assuming we do not backport to 10.0.100, this is when we expect the fix to reach users.


The C# compiler made a breaking change in order to properly adhere to the [ref safety rules](https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/collection-expressions.md#ref-safety) in the *collection expressions* feature specification. Specifically, the following clause:

> * If the target type is a *span type* `System.Span<T>` or `System.ReadOnlySpan<T>`, the safe-context of the collection expression is the *declaration-block*.

Previously, the compiler used safe-context *function-member* in this situation. We have now made a change to use *declaration-block* per the specification. This can cause new errors to appear in existing code, such as in the scenario below:

```cs
scoped Span<int> items1 = default;
scoped Span<int> items2 = default;
foreach (var x in new[] { 1, 2 })
{
Span<int> items = [x];
if (x == 1)
items1 = items; // previously allowed, now an error

if (x == 2)
items2 = items; // previously allowed, now an error
}
```

If your code is impacted by this breaking change, consider using an array type for the relevant collection expressions instead:

```cs
scoped Span<int> items1 = default;
scoped Span<int> items2 = default;
foreach (var x in new[] { 1, 2 })
{
int[] items = [x];
if (x == 1)
items1 = items; // ok, using 'int[]' conversion to 'Span<int>'

if (x == 2)
items2 = items; // ok
}
```

Alternatively, move the collection-expression to a scope where the assignment is permitted:
```cs
scoped Span<int> items1 = default;
scoped Span<int> items2 = default;
Span<int> items = [0];
foreach (var x in new[] { 1, 2 })
{
items[0] = x;
if (x == 1)
items1 = items; // ok

if (x == 2)
items2 = items; // ok
}
```

See also https://github.com/dotnet/csharplang/issues/9750.
4 changes: 2 additions & 2 deletions src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4585,7 +4585,7 @@ internal SafeContext GetValEscape(BoundExpression expr, SafeContext localScopeDe
if (conversion.ConversionKind == ConversionKind.CollectionExpression)
{
return HasLocalScope((BoundCollectionExpression)conversion.Operand) ?
SafeContext.CurrentMethod :
localScopeDepth :
SafeContext.CallingMethod;
}

Expand Down Expand Up @@ -5333,7 +5333,7 @@ internal bool CheckValEscape(SyntaxNode node, BoundExpression expr, SafeContext

if (conversion.ConversionKind == ConversionKind.CollectionExpression)
{
if (HasLocalScope((BoundCollectionExpression)conversion.Operand) && !SafeContext.CurrentMethod.IsConvertibleTo(escapeTo))
if (HasLocalScope((BoundCollectionExpression)conversion.Operand) && !escapeFrom.IsConvertibleTo(escapeTo))
{
Error(diagnostics, ErrorCode.ERR_CollectionExpressionEscape, node, expr.Type);
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26290,52 +26290,14 @@ static void F(bool b)
}
}
""";
var verifier = CompileAndVerify(
source,
targetFramework: targetFramework,
verify: Verification.Skipped,
expectedOutput: IncludeExpectedOutput(""));
if (targetFramework == TargetFramework.Net80)
{
verifier.VerifyIL("Program.F", """
{
// Code size 66 (0x42)
.maxstack 1
.locals init (object V_0,
object V_1,
object V_2,
object V_3)
IL_0000: ldc.i4.1
IL_0001: box "int"
IL_0006: stloc.0
IL_0007: ldloca.s V_0
IL_0009: newobj "System.ReadOnlySpan<object>..ctor(ref readonly object)"
IL_000e: pop
IL_000f: ldarg.0
IL_0010: brfalse.s IL_0023
IL_0012: ldc.i4.2
IL_0013: box "int"
IL_0018: stloc.1
IL_0019: ldloca.s V_1
IL_001b: newobj "System.ReadOnlySpan<object>..ctor(ref readonly object)"
IL_0020: pop
IL_0021: br.s IL_0032
IL_0023: ldc.i4.3
IL_0024: box "int"
IL_0029: stloc.2
IL_002a: ldloca.s V_2
IL_002c: newobj "System.ReadOnlySpan<object>..ctor(ref readonly object)"
IL_0031: pop
IL_0032: ldc.i4.4
IL_0033: box "int"
IL_0038: stloc.3
IL_0039: ldloca.s V_3
IL_003b: newobj "System.ReadOnlySpan<object>..ctor(ref readonly object)"
IL_0040: pop
IL_0041: ret
}
""");
}
var comp = CreateCompilation(source, targetFramework: targetFramework);
comp.VerifyEmitDiagnostics(
// (14,17): error CS9203: A collection expression of type 'ReadOnlySpan<object>' cannot be used in this context because it may be exposed outside of the current scope.
// x = [2];
Diagnostic(ErrorCode.ERR_CollectionExpressionEscape, "[2]").WithArguments("System.ReadOnlySpan<object>").WithLocation(14, 17),
// (19,17): error CS8352: Cannot use variable 'y' in this context because it may expose referenced variables outside of their declaration scope
// x = y;
Diagnostic(ErrorCode.ERR_EscapeVariable, "y").WithArguments("y").WithLocation(19, 17));
}

[Fact]
Expand Down Expand Up @@ -26364,70 +26326,14 @@ static void M<T>(bool b, T x, T y, T z, T w)
}
}
""";
var verifier = CompileAndVerify(
new[] { source, s_collectionExtensionsWithSpan },
targetFramework: TargetFramework.Net80,
verify: Verification.Skipped,
expectedOutput: IncludeExpectedOutput("[3, 4], "));
verifier.VerifyIL("Program.M<T>", """
{
// Code size 134 (0x86)
.maxstack 2
.locals init (System.Span<T> V_0, //s
<>y__InlineArray3<T> V_1,
<>y__InlineArray2<T> V_2,
System.ReadOnlySpan<T> V_3)
IL_0000: ldloca.s V_0
IL_0002: initobj "System.Span<T>"
IL_0008: ldarg.0
IL_0009: brfalse.s IL_0046
IL_000b: ldloca.s V_1
IL_000d: initobj "<>y__InlineArray3<T>"
IL_0013: ldloca.s V_1
IL_0015: ldc.i4.0
IL_0016: call "ref T <PrivateImplementationDetails>.InlineArrayElementRef<<>y__InlineArray3<T>, T>(ref <>y__InlineArray3<T>, int)"
IL_001b: ldarg.1
IL_001c: stobj "T"
IL_0021: ldloca.s V_1
IL_0023: ldc.i4.1
IL_0024: call "ref T <PrivateImplementationDetails>.InlineArrayElementRef<<>y__InlineArray3<T>, T>(ref <>y__InlineArray3<T>, int)"
IL_0029: ldarg.2
IL_002a: stobj "T"
IL_002f: ldloca.s V_1
IL_0031: ldc.i4.2
IL_0032: call "ref T <PrivateImplementationDetails>.InlineArrayElementRef<<>y__InlineArray3<T>, T>(ref <>y__InlineArray3<T>, int)"
IL_0037: ldarg.3
IL_0038: stobj "T"
IL_003d: ldloca.s V_1
IL_003f: ldc.i4.3
IL_0040: call "System.Span<T> <PrivateImplementationDetails>.InlineArrayAsSpan<<>y__InlineArray3<T>, T>(ref <>y__InlineArray3<T>, int)"
IL_0045: stloc.0
IL_0046: ldarg.0
IL_0047: brfalse.s IL_0077
IL_0049: ldloca.s V_2
IL_004b: initobj "<>y__InlineArray2<T>"
IL_0051: ldloca.s V_2
IL_0053: ldc.i4.0
IL_0054: call "ref T <PrivateImplementationDetails>.InlineArrayElementRef<<>y__InlineArray2<T>, T>(ref <>y__InlineArray2<T>, int)"
IL_0059: ldarg.3
IL_005a: stobj "T"
IL_005f: ldloca.s V_2
IL_0061: ldc.i4.1
IL_0062: call "ref T <PrivateImplementationDetails>.InlineArrayElementRef<<>y__InlineArray2<T>, T>(ref <>y__InlineArray2<T>, int)"
IL_0067: ldarg.s V_4
IL_0069: stobj "T"
IL_006e: ldloca.s V_2
IL_0070: ldc.i4.2
IL_0071: call "System.Span<T> <PrivateImplementationDetails>.InlineArrayAsSpan<<>y__InlineArray2<T>, T>(ref <>y__InlineArray2<T>, int)"
IL_0076: stloc.0
IL_0077: ldloc.0
IL_0078: call "System.ReadOnlySpan<T> System.Span<T>.op_Implicit(System.Span<T>)"
IL_007d: stloc.3
IL_007e: ldloca.s V_3
IL_0080: call "void CollectionExtensions.Report<T>(in System.ReadOnlySpan<T>)"
IL_0085: ret
}
""");
var comp = CreateCompilation(new[] { source, s_collectionExtensionsWithSpan }, targetFramework: TargetFramework.Net80);
comp.VerifyEmitDiagnostics(
// (13,17): error CS9203: A collection expression of type 'Span<T>' cannot be used in this context because it may be exposed outside of the current scope.
// s = [x, y, z];
Diagnostic(ErrorCode.ERR_CollectionExpressionEscape, "[x, y, z]").WithArguments("System.Span<T>").WithLocation(13, 17),
// (17,17): error CS9203: A collection expression of type 'Span<T>' cannot be used in this context because it may be exposed outside of the current scope.
// s = [z, w];
Diagnostic(ErrorCode.ERR_CollectionExpressionEscape, "[z, w]").WithArguments("System.Span<T>").WithLocation(17, 17));
}

[Fact]
Expand All @@ -26452,33 +26358,11 @@ static void M<T>(bool b, T[] a)
}
}
""";
var verifier = CompileAndVerify(
new[] { source, s_collectionExtensionsWithSpan },
targetFramework: TargetFramework.Net80,
verify: Verification.Skipped,
expectedOutput: IncludeExpectedOutput("[1, null, 3], "));
verifier.VerifyIL("Program.M<T>", """
{
// Code size 39 (0x27)
.maxstack 2
.locals init (System.Span<T> V_0, //s
System.ReadOnlySpan<T> V_1)
IL_0000: ldloca.s V_0
IL_0002: initobj "System.Span<T>"
IL_0008: ldarg.0
IL_0009: brfalse.s IL_0018
IL_000b: ldloca.s V_0
IL_000d: ldarg.1
IL_000e: call "T[] System.Linq.Enumerable.ToArray<T>(System.Collections.Generic.IEnumerable<T>)"
IL_0013: call "System.Span<T>..ctor(T[])"
IL_0018: ldloc.0
IL_0019: call "System.ReadOnlySpan<T> System.Span<T>.op_Implicit(System.Span<T>)"
IL_001e: stloc.1
IL_001f: ldloca.s V_1
IL_0021: call "void CollectionExtensions.Report<T>(in System.ReadOnlySpan<T>)"
IL_0026: ret
}
""");
var comp = CreateCompilation(new[] { source, s_collectionExtensionsWithSpan }, targetFramework: TargetFramework.Net80);
comp.VerifyEmitDiagnostics(
// (13,17): error CS9203: A collection expression of type 'Span<T>' cannot be used in this context because it may be exposed outside of the current scope.
// s = [..a];
Diagnostic(ErrorCode.ERR_CollectionExpressionEscape, "[..a]").WithArguments("System.Span<T>").WithLocation(13, 17));
}

[Fact]
Expand Down Expand Up @@ -26507,51 +26391,48 @@ static Action<bool, T, T, T> F<T>()
}
}
""";
var verifier = CompileAndVerify(
new[] { source, s_collectionExtensionsWithSpan },
targetFramework: TargetFramework.Net80,
verify: Verification.Skipped,
expectedOutput: IncludeExpectedOutput("[1, null, 3], "));
verifier.VerifyIL("Program.<>c__1<T>.<F>b__1_0(bool, T, T, T)", """
var comp = CreateCompilation(new[] { source, s_collectionExtensionsWithSpan }, targetFramework: TargetFramework.Net80);
comp.VerifyEmitDiagnostics(
// (16,26): error CS8352: Cannot use variable 's2' in this context because it may expose referenced variables outside of their declaration scope
// s1 = s2;
Diagnostic(ErrorCode.ERR_EscapeVariable, "s2").WithArguments("s2").WithLocation(16, 26));
}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80107")]
public void SpanAssignment_NestedScope_IssueExample()
{
// Test case from the issue description
string source = """
using System;
class Program
{
// Code size 86 (0x56)
.maxstack 2
.locals init (System.Span<T> V_0, //s1
<>y__InlineArray3<T> V_1,
System.ReadOnlySpan<T> V_2)
IL_0000: ldloca.s V_0
IL_0002: initobj "System.Span<T>"
IL_0008: ldarg.1
IL_0009: brfalse.s IL_0047
IL_000b: ldloca.s V_1
IL_000d: initobj "<>y__InlineArray3<T>"
IL_0013: ldloca.s V_1
IL_0015: ldc.i4.0
IL_0016: call "ref T <PrivateImplementationDetails>.InlineArrayElementRef<<>y__InlineArray3<T>, T>(ref <>y__InlineArray3<T>, int)"
IL_001b: ldarg.2
IL_001c: stobj "T"
IL_0021: ldloca.s V_1
IL_0023: ldc.i4.1
IL_0024: call "ref T <PrivateImplementationDetails>.InlineArrayElementRef<<>y__InlineArray3<T>, T>(ref <>y__InlineArray3<T>, int)"
IL_0029: ldarg.3
IL_002a: stobj "T"
IL_002f: ldloca.s V_1
IL_0031: ldc.i4.2
IL_0032: call "ref T <PrivateImplementationDetails>.InlineArrayElementRef<<>y__InlineArray3<T>, T>(ref <>y__InlineArray3<T>, int)"
IL_0037: ldarg.s V_4
IL_0039: stobj "T"
IL_003e: ldloca.s V_1
IL_0040: ldc.i4.3
IL_0041: call "System.Span<T> <PrivateImplementationDetails>.InlineArrayAsSpan<<>y__InlineArray3<T>, T>(ref <>y__InlineArray3<T>, int)"
IL_0046: stloc.0
IL_0047: ldloc.0
IL_0048: call "System.ReadOnlySpan<T> System.Span<T>.op_Implicit(System.Span<T>)"
IL_004d: stloc.2
IL_004e: ldloca.s V_2
IL_0050: call "void CollectionExtensions.Report<T>(in System.ReadOnlySpan<T>)"
IL_0055: ret
static void Main()
{
scoped Span<int> items1 = default;
scoped Span<int> items2 = default;
foreach (var x in new[] { 1, 2 })
{
Span<int> items = [x];
if (x == 1)
items1 = items;

if (x == 2)
items2 = items;
}

Console.Write(items1[0]);
Console.Write(items2[0]);
}
}
""");
""";
var comp = CreateCompilation(source, targetFramework: TargetFramework.Net80);
comp.VerifyEmitDiagnostics(
// (12,26): error CS8352: Cannot use variable 'items' in this context because it may expose referenced variables outside of their declaration scope
// items1 = items;
Diagnostic(ErrorCode.ERR_EscapeVariable, "items").WithArguments("items").WithLocation(12, 26),
// (15,26): error CS8352: Cannot use variable 'items' in this context because it may expose referenced variables outside of their declaration scope
// items2 = items;
Diagnostic(ErrorCode.ERR_EscapeVariable, "items").WithArguments("items").WithLocation(15, 26));
}

[Fact]
Expand Down