Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix modification of readonly locals through ref fields #74255

Merged
merged 5 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions src/Compilers/CSharp/Portable/BoundTree/Constructors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ private static bool NeedsByValueFieldAccess(BoundExpression? receiver, FieldSymb
{
if (fieldSymbol.IsStatic ||
!fieldSymbol.ContainingType.IsValueType ||
fieldSymbol.RefKind != RefKind.None ||
receiver == null) // receiver may be null in error cases
{
return false;
Expand Down
249 changes: 249 additions & 0 deletions src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenForEachTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5327,5 +5327,254 @@ public static class Extensions
}";
CompileAndVerify(source, parseOptions: TestOptions.Regular9, expectedOutput: "123123");
}

[Theory, CombinatorialData, WorkItem("https:/dotnet/roslyn/issues/73741")]
public void MutatingThroughRefFields_01(
[CombinatorialValues("ref", "")] string eRef,
[CombinatorialValues("readonly", "")] string vReadonly)
{
Copy link
Member

Choose a reason for hiding this comment

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

Consider adding a smoke test for LINQ iteration variables and using variables. They have the same "can't assign" semantics as foreach iteration variables and hence are possibly subject to the same issue.

Copy link
Member Author

@jjonescz jjonescz Jul 9, 2024

Choose a reason for hiding this comment

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

There are tests for using variables added in this PR in CodeGenUsingStatementTests.cs.
LINQ iteration variables aren't affected by this bug (the iteration variable is not a LocalSymbol so the BoundFieldAccess has always IsByValue set to false), but I will add similar tests.

var source = $$"""
using System;

V[] arr = new V[3];

foreach (var r in new E(arr))
{
r.V.F++;
}

foreach (var v in arr) Console.Write(v.F);

{{eRef}} struct E(V[] arr)
{
int i;
public E GetEnumerator() => this;
public R Current => new(ref arr[i - 1]);
public bool MoveNext() => i++ < arr.Length;
}

ref struct R(ref V v)
{
public {{vReadonly}} ref V V = ref v;
}

struct V
{
public int F;
}
""";
CompileAndVerify(source, targetFramework: TargetFramework.Net70,
verify: Verification.Fails,
expectedOutput: ExecutionConditionUtil.IsDesktop ? null : "111").VerifyDiagnostics();
}

[Theory, CombinatorialData, WorkItem("https:/dotnet/roslyn/issues/73741")]
public void MutatingThroughRefFields_02(
[CombinatorialValues("ref", "")] string eRef,
[CombinatorialValues("readonly", "")] string vReadonly)
{
var source = $$"""
using System;

V[] arr = new V[3];

foreach (var r in new E(arr))
{
r.V.F += 2;
}

foreach (var v in arr) Console.Write(v.F);

{{eRef}} struct E(V[] arr)
{
int i;
public E GetEnumerator() => this;
public R Current => new(ref arr[i - 1]);
public bool MoveNext() => i++ < arr.Length;
}

ref struct R(ref V v)
{
public {{vReadonly}} ref V V = ref v;
}

struct V
{
public int F;
}
""";
CompileAndVerify(source, targetFramework: TargetFramework.Net70,
verify: Verification.Fails,
expectedOutput: ExecutionConditionUtil.IsDesktop ? null : "222").VerifyDiagnostics();
}

[Theory, CombinatorialData, WorkItem("https:/dotnet/roslyn/issues/73741")]
public void MutatingThroughRefFields_03(
[CombinatorialValues("ref", "")] string eRef,
[CombinatorialValues("readonly", "")] string vReadonly)
{
var source = $$"""
using System;

V[] arr = new V[3];

foreach (var r in new E(arr))
{
r.V.S.Inc();
}

foreach (var v in arr) Console.Write(v.S.F);

{{eRef}} struct E(V[] arr)
{
int i;
public E GetEnumerator() => this;
public R Current => new(ref arr[i - 1]);
public bool MoveNext() => i++ < arr.Length;
}

ref struct R(ref V v)
{
public {{vReadonly}} ref V V = ref v;
}

struct V
{
public S S;
}

struct S
{
public int F;
public void Inc() => F++;
}
""";
CompileAndVerify(source, targetFramework: TargetFramework.Net70,
verify: Verification.Fails,
expectedOutput: ExecutionConditionUtil.IsDesktop ? null : "111").VerifyDiagnostics();
}

[Theory, CombinatorialData, WorkItem("https:/dotnet/roslyn/issues/73741")]
public void MutatingThroughRefFields_04(
[CombinatorialValues("ref", "")] string eRef,
[CombinatorialValues("readonly", "")] string vReadonly)
{
var source = $$"""
using System;

V[] arr = new V[3];

foreach (var r in new E(arr))
{
r.V.F++;
}

foreach (var v in arr) Console.Write(v.F);

{{eRef}} struct E(V[] arr)
{
int i;
public E GetEnumerator() => this;
public R Current => new(ref arr[i - 1]);
public bool MoveNext() => i++ < arr.Length;
}

ref struct R(ref V v)
{
public {{vReadonly}} ref readonly V V = ref v;
}

struct V
{
public int F;
}
""";
CreateCompilation(source, targetFramework: TargetFramework.Net70).VerifyDiagnostics(
// (7,5): error CS8332: Cannot assign to a member of field 'V' or use it as the right hand side of a ref assignment because it is a readonly variable
// r.V.F++;
Diagnostic(ErrorCode.ERR_AssignReadonlyNotField2, "r.V.F").WithArguments("field", "V").WithLocation(7, 5));
}

[Theory, CombinatorialData, WorkItem("https:/dotnet/roslyn/issues/73741")]
public void MutatingThroughRefFields_05(
[CombinatorialValues("ref", "")] string eRef,
[CombinatorialValues("readonly", "")] string vReadonly)
{
var source = $$"""
using System;

V[] arr = new V[3];

foreach (ref var r in new E(arr))
{
r.S.F++;
}

foreach (var v in arr) Console.Write(v.S.F);

{{eRef}} struct E(V[] arr)
{
int i;
public E GetEnumerator() => this;
public {{vReadonly}} ref V Current => ref arr[i - 1];
public bool MoveNext() => i++ < arr.Length;
}

struct V
{
public S S;
}

struct S
{
public int F;
}
""";
CompileAndVerify(source, targetFramework: TargetFramework.Net70,
verify: Verification.Skipped,
expectedOutput: ExecutionConditionUtil.IsDesktop ? null : "111").VerifyDiagnostics();
}

[Theory, CombinatorialData, WorkItem("https:/dotnet/roslyn/issues/73741")]
public void MutatingThroughRefFields_06(
[CombinatorialValues("ref", "")] string eRef,
[CombinatorialValues("readonly", "")] string vReadonly,
[CombinatorialValues("readonly", "")] string vReadonlyInner)
{
var source = $$"""
using System;

V[] arr = new V[3];

foreach (ref readonly var r in new E(arr))
{
r.S.F++;
}

foreach (var v in arr) Console.Write(v.S.F);

{{eRef}} struct E(V[] arr)
{
int i;
public E GetEnumerator() => this;
public {{vReadonly}} ref {{vReadonlyInner}} V Current => ref arr[i - 1];
public bool MoveNext() => i++ < arr.Length;
}

struct V
{
public S S;
}

struct S
{
public int F;
}
""";
CreateCompilation(source, targetFramework: TargetFramework.Net70).VerifyDiagnostics(
// (7,5): error CS1654: Cannot modify members of 'r' because it is a 'foreach iteration variable'
// r.S.F++;
Diagnostic(ErrorCode.ERR_AssgReadonlyLocal2Cause, "r.S.F").WithArguments("r", "foreach iteration variable").WithLocation(7, 5));
}
}
}
Loading
Loading