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 on anonymous types #53248

Merged
merged 13 commits into from
May 21, 2021
Merged

Allow with on anonymous types #53248

merged 13 commits into from
May 21, 2021

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented May 7, 2021

Test plan: #51199

Also includes:

  • disallow with on pointer types
  • don't produce an invalid operation in with on structures

@Youssef1313
Copy link
Member

Youssef1313 commented May 7, 2021

Should target main instead per #53125 (comment)?

We won't be using features/compiler anymore in the foreseeable future.


In reply to: 834059406

@jcouv jcouv changed the base branch from features/compiler to main May 7, 2021 04:41
@jcouv
Copy link
Member Author

jcouv commented May 7, 2021

Indeed. This can go in main now.


In reply to: 834059943


In reply to: 834059943

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented May 7, 2021

Can you check if the 'with' keyword recommender and intellisense for properties both work here? thanks!


In reply to: 834646583


In reply to: 834646583

Comment on lines +87 to 88
|| symbol.ContainingType.IsAnonymousType
|| CanSupportObjectInitializer(symbol);
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if the new check should be moved to CanSupportObjectInitializer?

Copy link
Member

Choose a reason for hiding this comment

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

i'm fine with either approach.

@jcouv jcouv marked this pull request as ready for review May 11, 2021 04:56
@jcouv jcouv requested review from a team as code owners May 11, 2021 04:56
@jcouv jcouv requested a review from CyrusNajmabadi May 11, 2021 04:56
@jcouv jcouv added the Area-IDE label May 11, 2021
@jcouv jcouv self-assigned this May 11, 2021
/// - Regions group blocks together and represent the lifetime of locals and captures, loosely similar to scopes in C#.
/// There are different kinds of regions, <see cref="ControlFlowRegionKind"/>.
/// - <see cref="ControlFlowGraphBuilder.SpillEvalStack"/> converts values on the stack into captures. It should be
/// called before entering a new region (to avoid doing it later thus giving those captures an incorrect lifetime).
Copy link
Contributor

@AlekseyTs AlekseyTs May 11, 2021

Choose a reason for hiding this comment

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

I do not think this is 100% accurate. I suggest removing this statement. What is the "correct" lifetime region depends on particular situation. #Closed

@@ -5624,7 +5633,12 @@ public override IOperation VisitFlowCapture(IFlowCaptureOperation operation, int

public override IOperation VisitFlowCaptureReference(IFlowCaptureReferenceOperation operation, int? captureIdForResult)
{
throw ExceptionUtilities.Unreachable;
Debug.Assert(operation.Parent is SimpleAssignmentOperation assignment
Copy link
Contributor

@AlekseyTs AlekseyTs May 11, 2021

Choose a reason for hiding this comment

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

Debug.Assert(operation.Parent is SimpleAssignmentOperation assignment

Is this code still reachable? #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.

Good catch. Restored exception.

IOperation cloned;
if (operation.Type.IsValueType)
{
cloned = visitedInstance;
Copy link
Contributor

@AlekseyTs AlekseyTs May 11, 2021

Choose a reason for hiding this comment

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

cloned = visitedInstance

Is this also implementing supprt for with on structures? It doesn't look like PR's description mentions that. #Closed

Copy link
Member Author

@jcouv jcouv May 11, 2021

Choose a reason for hiding this comment

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

Yes, I noticed that for structures we were producing an invalid operation because no clone method is available. I added a test and fixed it here, but forgot to add to description.

Updated description.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also includes fix for with on pointer types.

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'm thinking this fix may be incorrect though :-/ Do we need to capture it instead? I'll add some tests with control flow and invocations on value types....

Copy link
Member Author

Choose a reason for hiding this comment

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

Never mind. I recall that HandleObjectOrCollectionInitializer handles that.
I'll add the tests though


static bool setAllProperties(ImmutableArray<IOperation> initializers, ImmutableArray<IPropertySymbol> properties)
{
var set = PooledHashSet<string>.GetInstance();
Copy link
Contributor

@AlekseyTs AlekseyTs May 11, 2021

Choose a reason for hiding this comment

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

string

Why use name as the key instead of the property symbol? #Closed

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to have duplicate names in some error situations?

Copy link
Member Author

Choose a reason for hiding this comment

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

Name seems simpler.
Yes, it's possible to have duplicate names in error scenarios and this code handles that (we ignore the return value from Add and Remove)

Copy link
Contributor

Choose a reason for hiding this comment

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

Name seems simpler.
Yes, it's possible to have duplicate names in error scenarios and this code handles that (we ignore the return value from Add and Remove)

Does that mean that we will ignore the fact that the duplicate property isn't initialized? And perhaps not include its copy into the AnonymousObjectCreationOperation, thus failing to match the shape of the constructor?

// calls to Visit may enter regions, so we reset things
LeaveRegionsUpTo(innerCaptureRegion);

var explicitProperties = PooledDictionary<string, IOperation>.GetInstance();
Copy link
Contributor

@AlekseyTs AlekseyTs May 11, 2021

Choose a reason for hiding this comment

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

string

Using names as keys feels fragile, I think we should use property symbols for that. #Closed

simpleAssignment.Target is not InvalidOperation)
{
var propertyReference = (IPropertyReferenceOperation)simpleAssignment.Target;
_ = set.Remove(propertyReference.Property);
Copy link
Contributor

@AlekseyTs AlekseyTs May 12, 2021

Choose a reason for hiding this comment

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

set.Remove(propertyReference.Property);

It looks like we are making an assumption that a property that we get here belongs to the Anonymous Type that is cloned (otherwise we might be allocating captures that won't be used at the end). I think it is a valid assumption to make, but it might be worth adding an assert about that several lines above, where we asert other aspects of each property reference. Based on that assumption, consider if it is worth reducing the amount of HashSet operations (additions/removals) that we perform. For example, instead of first adding all properties into the set and then removing those that we run into, we could start with an empty set, add properties that we run into and at the end check if the count of items in the set equals to the count of properties in the type. #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented May 12, 2021

    public void WithExprOnStruct_ControlFlow_NestedInitializer()

I think we would actually want to see what happens with the Control Flow Graph for this error scenario. My understanding is that the purpose of the test is to make sure the builder doesn't crash and the graph looks reasonable.


In reply to: 839812576


In reply to: 839812576


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs:6596 in f930d4b. [](commit_id = f930d4b, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented May 12, 2021

    var b = a with { Error = Identity(20) };

Consider testing a scenario when the target of an assignment is a name of a function that actually exists in the type. "ToString" for example.


In reply to: 839819415


In reply to: 839819415


In reply to: 839819415


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs:8998 in f930d4b. [](commit_id = f930d4b, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented May 12, 2021

    public void WithExpr_AnonymousType_NestedInitializer()

I think we would actually want to see what happens with the Control Flow Graph for this error scenario.


In reply to: 839819719


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs:9103 in f930d4b. [](commit_id = f930d4b, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented May 12, 2021

    public void WithExpr_AnonymousType_IndexerAccess()

I think we would actually want to see what happens with the Control Flow Graph for this error scenario.


In reply to: 839820875


In reply to: 839820875


In reply to: 839820875


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs:9266 in f930d4b. [](commit_id = f930d4b, deletion_comment = False)

}
LeaveRegionsUpTo(outerCaptureRegion);

return new AnonymousObjectCreationOperation(initializerBuilder.ToImmutableAndFree(), semanticModel: null, operation.Syntax, operation.Type, operation.IsImplicit);
Copy link
Contributor

@AlekseyTs AlekseyTs May 12, 2021

Choose a reason for hiding this comment

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

initializerBuilder

Consider adding an assert that the amount of initializers matches the amount of properties. #Pending

&& withExpr.Initializer.Expressions.Any()
&& withExpr.Expression == (object)syntax)
{
return true;
Copy link
Contributor

@AlekseyTs AlekseyTs May 12, 2021

Choose a reason for hiding this comment

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

return true;

This is for scenarios like WithExprOnStruct_ControlFlow_WithCoalescingExpressionForValue so that the verifier won't complain about the long-lived capture with "Operation [{operationIndex}] in [{getBlockId(block)}] uses capture [{id.Value}] from another region. Should the regions be merged?" assertion above.

This would be more informative if the memo actually included the actual ids so that we could look at the graph and confirm that we indeed want to suppress the assertion. #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented May 12, 2021

                    IFlowCaptureReferenceOperation: 0 (OperationKind.FlowCaptureReference, Type: B, IsImplicit) (Syntax: 'Identity(this)')

Is this the capture reference that ControlFlowGraphVerifier was complaining about?


In reply to: 839841592


In reply to: 839841592


In reply to: 839841592


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs:6953 in f930d4b. [](commit_id = f930d4b, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 9)

@jcouv
Copy link
Member Author

jcouv commented May 12, 2021

                    IFlowCaptureReferenceOperation: 0 (OperationKind.FlowCaptureReference, Type: B, IsImplicit) (Syntax: 'Identity(this)')

Yes (capture 0 used in B5)


In reply to: 839841592


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs:6953 in f930d4b. [](commit_id = f930d4b, deletion_comment = False)

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
Copy link
Member Author

jcouv commented May 12, 2021

@dotnet/roslyn-compiler for a second review. Thanks

static bool isAllowedDespiteReadonly(BoundExpression receiver)
{
// ok: anonymousType with { Property = ... }
if (receiver is BoundObjectOrCollectionValuePlaceholder && receiver.Type.IsAnonymousType)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: it feels like the && can be collapsed into a single recursive pattern and this can be inlined at the call site :)

ImmutableArray<BoundExpression> getAnonymousTypeValues(BoundWithExpression withExpr, BoundExpression oldValue, AnonymousTypeManager.AnonymousTypePublicSymbol anonymousType,
ArrayBuilder<BoundExpression> sideEffects, ArrayBuilder<LocalSymbol> temps)
{
var dictionary = PooledDictionary<PropertySymbol, BoundExpression>.GetInstance();
Copy link
Contributor

@RikkiGibson RikkiGibson May 18, 2021

Choose a reason for hiding this comment

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

nit: it feels like in this scenario we could use an ArrayBuilder which maps the AnonymousTypePropertySymbol.MemberIndexOpt.Value to a BoundLocal?. Also could consider exposing a member on AnonymousTypePropertySymbol like int MemberIndex => _index. #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.

Made the change. Thanks

@@ -1881,6 +1881,7 @@ protected static void VerifyFlowGraphForTest<TSyntaxNode>(CSharpCompilation comp
{
var tree = compilation.SyntaxTrees[0];
SyntaxNode syntaxNode = GetSyntaxNodeOfTypeForBinding<TSyntaxNode>(GetSyntaxNodeList(tree));
Debug.Assert(syntaxNode is not null, "Did you forget to place /*<bind>*/ comments in your source?");
Copy link
Contributor

@RikkiGibson RikkiGibson May 18, 2021

Choose a reason for hiding this comment

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

Thank you for this. #Closed

Debug.Assert(left.MemberSymbol is not null);

// We evaluate the values provided in source first
BoundLocal valueTemp = _factory.StoreToTemp(assignment.Right, out BoundAssignmentOperator boundAssignmentToTemp);
Copy link
Contributor

@RikkiGibson RikkiGibson May 18, 2021

Choose a reason for hiding this comment

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

nit: it feels like it's possible to avoid storing to temps when the assignments occur in the same order as the constructor parameters, similar to the way method call arguments are rewritten during lowering. However, I don't think it's necessary to implement this optimization in this PR. #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.

Agreed. Also, the cases where this optimization could actually play out aren't the common case so it seems fine to punt.

/// There are different kinds of regions, <see cref="ControlFlowRegionKind"/>.
/// - <see cref="ControlFlowGraphBuilder.SpillEvalStack"/> converts values on the stack into captures.
/// - Error scenarios from initial binding need to be handled.
/// </summary>
Copy link
Contributor

@RikkiGibson RikkiGibson May 18, 2021

Choose a reason for hiding this comment

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

Thanks for this. It helped me review this PR and I'm sure it will help future readers. #Closed

Copy link
Member

@CyrusNajmabadi CyrusNajmabadi left a comment

Choose a reason for hiding this comment

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

ide side lgmt.

@jcouv
Copy link
Member Author

jcouv commented May 20, 2021

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@jcouv jcouv merged commit 828c0a0 into dotnet:main May 21, 2021
@ghost ghost modified the milestones: C# 10, Next May 21, 2021
@jcouv jcouv deleted the with-anonymous branch May 21, 2021 03:48
333fred added a commit that referenced this pull request May 24, 2021
…ures/interpolated-string

* upstream/main: (92 commits)
  Keep casts when necessary to prefer a constant pattern over a type pattern
  Remove SyntaxKind.DataKeyword (#53614)
  Display 'readonly' for record structs (#53634)
  Update Building, Debugging, and Testing on Windows.md (#53543)
  Update dependencies from https://github.com/dotnet/arcade build 20210521.3 (#53617)
  Introduce resx for BuildValidator and MS.CA.Rebuild to allow localization (#53447)
  Report obsoletion diagnostics for slice and indexer (#53463)
  Update BasicGenerateConstructorDialog.cs
  Add searchbox in generate overrides dialog
  Allow `with` on anonymous types (#53248)
  Report diagnostic on correct node (#53538)
  Fix NotNullIfNotNull delegate conversion (#53409)
  Verify quick info session in InvokeQuickInfo
  Remove unnecessary retry
  Ensure no navbar IO on the UI thread
  Enable nullable reference types
  Fix timeout behavior in GetQuickInfo
  Add a semantic model based GetQuickInfoAsync entry point into QuickInfoServiceWithProviders
  Move semantic model based quick info API up to CommonQuickInfoProvider type
  Fix dnceng build by forcing the use of xcopy msbuild
  ...
@RikkiGibson RikkiGibson modified the milestones: Next, 17.0.P2 Jun 29, 2021
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.

5 participants