-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Conversation
@@ -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"> |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var src = @" | ||
var b = new B() { X = 1 }; | ||
var b2 = b.M(); | ||
System.Console.Write(b2.X); |
There was a problem hiding this comment.
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>)", @" |
There was a problem hiding this comment.
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 }; |
There was a problem hiding this comment.
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
There was a problem hiding this 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.
@@ -32,15 +32,19 @@ private BoundExpression BindWithExpression(WithExpressionSyntax syntax, BindingD | |||
} | |||
|
|||
MethodSymbol? cloneMethod = null; | |||
if (!receiverType.IsErrorType()) | |||
if (receiverType.IsValueType) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
@@ -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: |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM (commit 10)
Test plan #51199