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
Merged
Show file tree
Hide file tree
Changes from 6 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
5 changes: 3 additions & 2 deletions docs/contributing/Compiler Test Plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ This document provides guidance for thinking about language interactions and tes

# Type and members
- Access modifiers (public, protected, internal, protected internal, private protected, private), static, ref
- type declarations (class, record with or without positional members, struct, interface, type parameter)
- type declarations (class, record class/struct with or without positional members, struct, interface, type parameter)
- methods
- fields
- properties (including get/set/init accessors)
Expand Down Expand Up @@ -91,8 +91,9 @@ This document provides guidance for thinking about language interactions and tes
- Ref return, ref readonly return, ref ternary, ref readonly local, ref local re-assignment, ref foreach
- `this = e;` in `struct` .ctor
- Stackalloc (including initializers)
- Patterns (constant, declaration, `var`, positional, property, and discard forms)
- Patterns (constant, declaration, `var`, positional, property, discard, parenthesized, type, relational, `and`/`or`/`not`)
- Switch expressions
- With expressions (on record classes and on value types)
- Nullability annotations (`?`, attributes) and analysis
- If you add a place an expression can appear in code, make sure `SpillSequenceSpiller` handles it. Test with a `switch` expression or `stackalloc` in that place.
- If you add a new expression form that requires spilling, test it in the catch filter.
Expand Down
74 changes: 45 additions & 29 deletions src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3006,37 +3006,39 @@ internal static bool CheckValEscape(SyntaxNode node, BoundExpression expr, uint
isRefEscape: false);

case BoundKind.ObjectCreationExpression:
var objectCreation = (BoundObjectCreationExpression)expr;
var constructorSymbol = objectCreation.Constructor;

var escape = CheckInvocationEscape(
objectCreation.Syntax,
constructorSymbol,
null,
constructorSymbol.Parameters,
objectCreation.Arguments,
objectCreation.ArgumentRefKindsOpt,
objectCreation.ArgsToParamsOpt,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: false);

var initializerExpr = objectCreation.InitializerExpressionOpt;
if (initializerExpr != null)
{
escape = escape &&
CheckValEscape(
initializerExpr.Syntax,
initializerExpr,
escapeFrom,
escapeTo,
checkingReceiver: false,
diagnostics: diagnostics);
}
var objectCreation = (BoundObjectCreationExpression)expr;
var constructorSymbol = objectCreation.Constructor;

var escape = CheckInvocationEscape(
objectCreation.Syntax,
constructorSymbol,
null,
constructorSymbol.Parameters,
objectCreation.Arguments,
objectCreation.ArgumentRefKindsOpt,
objectCreation.ArgsToParamsOpt,
checkingReceiver,
escapeFrom,
escapeTo,
diagnostics,
isRefEscape: false);

var initializerExpr = objectCreation.InitializerExpressionOpt;
if (initializerExpr != null)
{
escape = escape &&
CheckValEscape(
initializerExpr.Syntax,
initializerExpr,
escapeFrom,
escapeTo,
checkingReceiver: false,
diagnostics: diagnostics);
}

return escape;
return escape;
}

case BoundKind.UnaryOperator:
var unary = (BoundUnaryOperator)expr;
Expand Down Expand Up @@ -3123,6 +3125,20 @@ internal static bool CheckValEscape(SyntaxNode node, BoundExpression expr, uint

return true;

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)


var initializerExpr = withExpr.InitializerExpression;
if (initializerExpr != null)
{
escape = escape && CheckValEscape(initializerExpr.Syntax, initializerExpr, escapeFrom, escapeTo, checkingReceiver: false, diagnostics: diagnostics);
}

return escape;
}

default:
// in error situations some unexpected nodes could make here
// returning "false" seems safer than throwing.
Expand Down
8 changes: 6 additions & 2 deletions src/Compilers/CSharp/Portable/Binder/Binder_WithExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

{
CheckFeatureAvailability(syntax, MessageID.IDS_FeatureWithOnStructs, diagnostics);
}
else if (!receiverType.IsErrorType())
{
CompoundUseSiteInfo<AssemblySymbol> useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics);

cloneMethod = SynthesizedRecordClone.FindValidCloneMethod(receiverType is TypeParameterSymbol typeParameter ? typeParameter.EffectiveBaseClass(ref useSiteInfo) : receiverType, ref useSiteInfo);
if (cloneMethod is null)
{
hasErrors = true;
diagnostics.Add(ErrorCode.ERR_NoSingleCloneMethod, syntax.Expression.Location, receiverType);
diagnostics.Add(ErrorCode.ERR_CannotClone, syntax.Expression.Location, receiverType);
}
else
{
Expand Down
7 changes: 5 additions & 2 deletions src/Compilers/CSharp/Portable/CSharpResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -6274,8 +6274,8 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<data name="ERR_InvalidWithReceiverType" xml:space="preserve">
<value>The receiver of a `with` expression must have a non-void type.</value>
</data>
<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)

</data>
<data name="ERR_AssignmentInitOnly" xml:space="preserve">
<value>Init-only property or indexer '{0}' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.</value>
Expand Down Expand Up @@ -6567,6 +6567,9 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<value>record structs</value>
<comment>'record structs' is not localizable.</comment>
</data>
<data name="IDS_FeatureWithOnStructs" xml:space="preserve">
<value>with on structs</value>
</data>
<data name="IDS_FeatureVarianceSafetyForStaticInterfaceMembers" xml:space="preserve">
<value>variance safety for static interface members</value>
</data>
Expand Down
2 changes: 1 addition & 1 deletion src/Compilers/CSharp/Portable/Errors/ErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1860,7 +1860,7 @@ internal enum ErrorCode
ERR_ExplicitPropertyMismatchInitOnly = 8855,
ERR_BadInitAccessor = 8856,
ERR_InvalidWithReceiverType = 8857,
ERR_NoSingleCloneMethod = 8858,
ERR_CannotClone = 8858,
ERR_CloneDisallowedInRecord = 8859,
WRN_RecordNamedDisallowed = 8860,
ERR_UnexpectedArgumentList = 8861,
Expand Down
4 changes: 3 additions & 1 deletion src/Compilers/CSharp/Portable/Errors/MessageID.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ internal enum MessageID
IDS_FeatureConstantInterpolatedStrings = MessageBase + 12792,
IDS_FeatureMixedDeclarationsAndExpressionsInDeconstruction = MessageBase + 12793,
IDS_FeatureRecordStructs = MessageBase + 12794,
IDS_FeatureWithOnStructs = MessageBase + 12795,
}

// Message IDs may refer to strings that need to be localized.
Expand Down Expand Up @@ -325,7 +326,8 @@ internal static LanguageVersion RequiredVersion(this MessageID feature)
{
// C# preview features.
case MessageID.IDS_FeatureRecordStructs:
case MessageID.IDS_FeatureMixedDeclarationsAndExpressionsInDeconstruction:
case MessageID.IDS_FeatureWithOnStructs: // semantic check
case MessageID.IDS_FeatureMixedDeclarationsAndExpressionsInDeconstruction: // semantic check
return LanguageVersion.Preview;
// C# 9.0 features.
case MessageID.IDS_FeatureLambdaDiscardParameters: // semantic check
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,31 +103,48 @@ public override BoundNode VisitObjectCreationExpression(BoundObjectCreationExpre

public override BoundNode VisitWithExpression(BoundWithExpression withExpr)
{
RoslynDebug.AssertNotNull(withExpr.CloneMethod);
Debug.Assert(withExpr.CloneMethod.ParameterCount == 0);
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, duplicate the value, then set the given struct properties:
//
// var tmp = receiver;
// tmp.P1 = e1;
// tmp.P2 = e2;
// tmp
//
// otherwise the receiver is a record class and we want to lower it to a call to its `Clone` method, then
// set the given record properties. i.e.
//
// var tmp = (ReceiverType)receiver.Clone();
// tmp.P1 = e1;
// tmp.P2 = e2;
// tmp

var cloneCall = _factory.Convert(
withExpr.Type,
_factory.Call(
VisitExpression(withExpr.Receiver),
withExpr.CloneMethod));
BoundExpression expression;

if (withExpr.Type.IsValueType)
{
expression = VisitExpression(withExpr.Receiver);
}
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

Debug.Assert(withExpr.CloneMethod.ParameterCount == 0);

expression = _factory.Convert(
withExpr.Type,
_factory.Call(
VisitExpression(withExpr.Receiver),
withExpr.CloneMethod));
}

return MakeExpressionWithInitializer(
withExpr.Syntax,
cloneCall,
expression,
withExpr.InitializerExpression,
withExpr.Type);
}
Expand Down
15 changes: 10 additions & 5 deletions src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 10 additions & 5 deletions src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 10 additions & 5 deletions src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 10 additions & 5 deletions src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading