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

Allow 'with' expression on value types #52319

Merged
merged 11 commits into from
Apr 7, 2021

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Mar 31, 2021

Test plan #51199

@jcouv jcouv added this to the C# 10 milestone Mar 31, 2021
@jcouv jcouv self-assigned this Mar 31, 2021
@jcouv jcouv mentioned this pull request Mar 31, 2021
92 tasks
@jcouv jcouv marked this pull request as ready for review April 2, 2021 14:37
@jcouv jcouv requested a review from a team as a code owner April 2, 2021 14:37
@jcouv jcouv marked this pull request as draft April 2, 2021 18:07
@jcouv jcouv marked this pull request as ready for review April 2, 2021 19:31
@@ -6275,7 +6275,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<value>The receiver of a `with` expression must have a non-void type.</value>
</data>
<data name="ERR_NoSingleCloneMethod" xml:space="preserve">
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 6, 2021

Choose a reason for hiding this comment

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

ERR_NoSingleCloneMethod [](start = 14, length = 23)

Consider renaming the error to more accurately reflect its purpose/conditions under which it is reported #Closed

case BoundKind.WithExpression:
{
var withExpr = (BoundWithExpression)expr;
var escape = CheckValEscape(node, withExpr.Receiver, escapeFrom, escapeTo, checkingReceiver: false, diagnostics);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 6, 2021

Choose a reason for hiding this comment

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

CheckValEscape(node, withExpr.Receiver, escapeFrom, escapeTo, checkingReceiver: false, diagnostics); [](start = 37, length = 100)

Were we missing this check for 'with' on records? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

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

From what I understand, val escape analyses are initiated when we do an assignment (we call GetValEscape and ValidateEscape from BindAssignment, BindDeconstructionAssignment, BindReturn, etc). But it's only relevant if we're assigning a ref-like type (GetValEscape returns ExternalScope for all other types).
So I think record structs introduce the need to analyze with expressions, because the type of the receiver and the with expression now can be a ref-like struct.

For the initializer part, I'm not sure. I've added a test WithExpr_ValEscape which would have thought should produce diagnostics because the initializer contains dangerous assignments. Could you take a look?
If I'm correct, this is an existing bug (an assignment for which we are not triggering val-escape checks).


In reply to: 607968314 [](ancestors = 607968314)

Copy link
Member Author

Choose a reason for hiding this comment

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

Chatted with Jared. The scenario in WithExpr_ValEscape with assigning a ref struct value to a property is fine. The setter is essentially void Set(S1), which cannot do anything dangerous (it could not store S1 and it returns void so may not sneak it out).


In reply to: 608258240 [](ancestors = 608258240,607968314)

Debug.Assert(withExpr.Receiver.Type!.Equals(withExpr.Type, TypeCompareKind.ConsiderEverything));

// for a with expression of the form
//
// receiver with { P1 = e1, P2 = e2 }
//
// we want to lower it to a call to the receiver's `Clone` method, then
// if the receiver is a struct, then set the given struct properties:
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 6, 2021

Choose a reason for hiding this comment

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

given [](start = 57, length = 5)

This sounds somewhat confusing. We are not mutating the given struct, but rather returning a new value. #Closed

Copy link
Member Author

Choose a reason for hiding this comment

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

The sentence was incomplete. Fixed


In reply to: 607973334 [](ancestors = 607973334)

var src = @"
var b = new B() { X = 1 };
var b2 = b.M();
System.Console.Write(b2.X);
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 6, 2021

Choose a reason for hiding this comment

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

Consider observing that the original structure is not mutated. #Closed

var comp = CreateCompilation(src);
comp.VerifyDiagnostics();
var verifier = CompileAndVerify(comp, expectedOutput: "4243");
verifier.VerifyIL("<Program>$.<<Main>$>g__M|0_0(System.ValueTuple<int, int>)", @"
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 6, 2021

Choose a reason for hiding this comment

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

$.<

$>g__M|0_0(System.ValueTuple<int, int>) [](start = 31, length = 57)

It feels like it would be better to have a regualr method within a type. #Closed

public readonly int X;
public B M()
{
return this with { X = 42 };
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 6, 2021

Choose a reason for hiding this comment

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

this with { X = 42 }; [](start = 15, length = 21)

Consider testing this error inside a constructor. #Closed

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 4) with some comments.

@RikkiGibson RikkiGibson self-assigned this Apr 6, 2021
@@ -32,15 +32,19 @@ private BoundExpression BindWithExpression(WithExpressionSyntax syntax, BindingD
}

MethodSymbol? cloneMethod = null;
if (!receiverType.IsErrorType())
if (receiverType.IsValueType)
Copy link
Contributor

@RikkiGibson RikkiGibson Apr 6, 2021

Choose a reason for hiding this comment

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

It feels like for enums for example there is nothing you can assign in the right side of the with. Is there any item tracking giving a warning when the right side of the with is empty? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

I have a couple of tests with an empty initializers in with. I don't think that should be a warning, as empty initializers are also allowed in object creation.


In reply to: 608246244 [](ancestors = 608246244)

}
else
{
RoslynDebug.AssertNotNull(withExpr.CloneMethod);
Copy link
Contributor

@RikkiGibson RikkiGibson Apr 6, 2021

Choose a reason for hiding this comment

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

nit: We should be using an annotated Debug.Assert at this point, so it should be possible to just say Debug.Assert(withExpr.CloneMethod is not null) #Resolved

<data name="ERR_NoSingleCloneMethod" xml:space="preserve">
<value>The receiver type '{0}' is not a valid record type.</value>
<data name="ERR_CannotClone" xml:space="preserve">
<value>The receiver type '{0}' is not a valid record type and is not a struct type.</value>
Copy link
Contributor

@RikkiGibson RikkiGibson Apr 6, 2021

Choose a reason for hiding this comment

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

Consider adding a <comment> that 'record' and 'struct' are language keywords for loc team #WontFix

Copy link
Member Author

Choose a reason for hiding this comment

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

In general, I'm not sure about such comments. In this instance, they feel wrong. record and struct are not keywords in this sentence.
If they were keywords, then they could be marked with backticks in markdown, but that doesn't work here: "...is not a valid record type and is not a struct type."


In reply to: 608248749 [](ancestors = 608248749)

@jcouv jcouv requested a review from AlekseyTs April 7, 2021 00:24
@@ -2724,6 +2724,12 @@ internal static uint GetValEscape(BoundExpression expr, uint scopeOfTheContainin
var switchExpr = (BoundSwitchExpression)expr;
return GetValEscape(switchExpr.SwitchArms.SelectAsArray(a => a.Value), scopeOfTheContainingExpression);

case BoundKind.WithExpression:
Copy link
Contributor

@AlekseyTs AlekseyTs Apr 7, 2021

Choose a reason for hiding this comment

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

case BoundKind.WithExpression: [](start = 16, length = 30)

It might be convenient to place this case next to BoundKind.ObjectCreationExpression: since they both have to deal with an initializer. Same for the other function getting a similar case. #Closed

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 10)

@jcouv jcouv enabled auto-merge (squash) April 7, 2021 19:19
@jcouv jcouv merged commit a959cbd into dotnet:features/record-structs Apr 7, 2021
@jcouv jcouv deleted the with-structs branch April 8, 2021 00:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants