diff --git a/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 11.md b/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 11.md new file mode 100644 index 0000000000000..150a02840c4fc --- /dev/null +++ b/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 11.md @@ -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*** + +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` or `System.ReadOnlySpan`, 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 items1 = default; +scoped Span items2 = default; +foreach (var x in new[] { 1, 2 }) +{ + Span 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 items1 = default; +scoped Span items2 = default; +foreach (var x in new[] { 1, 2 }) +{ + int[] items = [x]; + if (x == 1) + items1 = items; // ok, using 'int[]' conversion to 'Span' + + if (x == 2) + items2 = items; // ok +} +``` + +Alternatively, move the collection-expression to a scope where the assignment is permitted: +```cs +scoped Span items1 = default; +scoped Span items2 = default; +Span 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. diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs index 54f3731f4a1c4..1c07bb9af6e97 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs @@ -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; } @@ -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; diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests.cs index 0a1989896e29c..402e285945905 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/CollectionExpressionTests.cs @@ -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..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..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..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..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' 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").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] @@ -26364,70 +26326,14 @@ static void M(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", """ - { - // Code size 134 (0x86) - .maxstack 2 - .locals init (System.Span V_0, //s - <>y__InlineArray3 V_1, - <>y__InlineArray2 V_2, - System.ReadOnlySpan V_3) - IL_0000: ldloca.s V_0 - IL_0002: initobj "System.Span" - IL_0008: ldarg.0 - IL_0009: brfalse.s IL_0046 - IL_000b: ldloca.s V_1 - IL_000d: initobj "<>y__InlineArray3" - IL_0013: ldloca.s V_1 - IL_0015: ldc.i4.0 - IL_0016: call "ref T .InlineArrayElementRef<<>y__InlineArray3, T>(ref <>y__InlineArray3, 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 .InlineArrayElementRef<<>y__InlineArray3, T>(ref <>y__InlineArray3, 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 .InlineArrayElementRef<<>y__InlineArray3, T>(ref <>y__InlineArray3, 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 .InlineArrayAsSpan<<>y__InlineArray3, T>(ref <>y__InlineArray3, 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" - IL_0051: ldloca.s V_2 - IL_0053: ldc.i4.0 - IL_0054: call "ref T .InlineArrayElementRef<<>y__InlineArray2, T>(ref <>y__InlineArray2, 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 .InlineArrayElementRef<<>y__InlineArray2, T>(ref <>y__InlineArray2, 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 .InlineArrayAsSpan<<>y__InlineArray2, T>(ref <>y__InlineArray2, int)" - IL_0076: stloc.0 - IL_0077: ldloc.0 - IL_0078: call "System.ReadOnlySpan System.Span.op_Implicit(System.Span)" - IL_007d: stloc.3 - IL_007e: ldloca.s V_3 - IL_0080: call "void CollectionExtensions.Report(in System.ReadOnlySpan)" - 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' 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").WithLocation(13, 17), + // (17,17): error CS9203: A collection expression of type 'Span' 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").WithLocation(17, 17)); } [Fact] @@ -26452,33 +26358,11 @@ static void M(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", """ - { - // Code size 39 (0x27) - .maxstack 2 - .locals init (System.Span V_0, //s - System.ReadOnlySpan V_1) - IL_0000: ldloca.s V_0 - IL_0002: initobj "System.Span" - 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(System.Collections.Generic.IEnumerable)" - IL_0013: call "System.Span..ctor(T[])" - IL_0018: ldloc.0 - IL_0019: call "System.ReadOnlySpan System.Span.op_Implicit(System.Span)" - IL_001e: stloc.1 - IL_001f: ldloca.s V_1 - IL_0021: call "void CollectionExtensions.Report(in System.ReadOnlySpan)" - 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' 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").WithLocation(13, 17)); } [Fact] @@ -26507,51 +26391,48 @@ static Action F() } } """; - var verifier = CompileAndVerify( - new[] { source, s_collectionExtensionsWithSpan }, - targetFramework: TargetFramework.Net80, - verify: Verification.Skipped, - expectedOutput: IncludeExpectedOutput("[1, null, 3], ")); - verifier.VerifyIL("Program.<>c__1.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 V_0, //s1 - <>y__InlineArray3 V_1, - System.ReadOnlySpan V_2) - IL_0000: ldloca.s V_0 - IL_0002: initobj "System.Span" - IL_0008: ldarg.1 - IL_0009: brfalse.s IL_0047 - IL_000b: ldloca.s V_1 - IL_000d: initobj "<>y__InlineArray3" - IL_0013: ldloca.s V_1 - IL_0015: ldc.i4.0 - IL_0016: call "ref T .InlineArrayElementRef<<>y__InlineArray3, T>(ref <>y__InlineArray3, 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 .InlineArrayElementRef<<>y__InlineArray3, T>(ref <>y__InlineArray3, 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 .InlineArrayElementRef<<>y__InlineArray3, T>(ref <>y__InlineArray3, 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 .InlineArrayAsSpan<<>y__InlineArray3, T>(ref <>y__InlineArray3, int)" - IL_0046: stloc.0 - IL_0047: ldloc.0 - IL_0048: call "System.ReadOnlySpan System.Span.op_Implicit(System.Span)" - IL_004d: stloc.2 - IL_004e: ldloca.s V_2 - IL_0050: call "void CollectionExtensions.Report(in System.ReadOnlySpan)" - IL_0055: ret + static void Main() + { + scoped Span items1 = default; + scoped Span items2 = default; + foreach (var x in new[] { 1, 2 }) + { + Span 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]