-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Deconstruction-assignment for tuples #11457
Changes from 4 commits
6967473
dcb9c69
a2c290b
4f443f0
e046522
7170bd2
227c3a5
1688f5f
66662a3
c7b60a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1715,6 +1715,13 @@ private BoundExpression BindAssignment(AssignmentExpressionSyntax node, Diagnost | |
return BindAssignment(node, op1, op2, diagnostics); | ||
} | ||
|
||
/// <summary> | ||
/// There are two kinds of deconstruction-assignments which this binding handles: tuple and non-tuple. | ||
/// | ||
/// Returns a BoundDeconstructionAssignmentOperator | ||
/// - with all the fields populated except deconstructMember, for the tuple case | ||
/// - with all the fields populated, for a non-tuple case | ||
/// </summary> | ||
private BoundExpression BindDeconstructionAssignment(AssignmentExpressionSyntax node, DiagnosticBag diagnostics) | ||
{ | ||
SeparatedSyntaxList<ArgumentSyntax> arguments = ((TupleExpressionSyntax)node.Left).Arguments; | ||
|
@@ -1737,6 +1744,52 @@ private BoundExpression BindDeconstructionAssignment(AssignmentExpressionSyntax | |
|
||
var checkedVariables = checkedVariablesBuilder.ToImmutableAndFree(); | ||
|
||
// tuple literal such as `(1, 2)`, `(null, null)`, `(x.P, y.M())` | ||
if (boundRHS.Kind == BoundKind.TupleLiteral || ((object)boundRHS.Type != null && boundRHS.Type.IsTupleType)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the first condition necessary or would a literal also satisfy the second condition? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason for the first condition is tuple literals that don't have a natural type, such as |
||
{ | ||
return BindDeconstructWithTuple(node, diagnostics, boundRHS, checkedVariables); | ||
} | ||
|
||
// expression without type such as `null` | ||
if (boundRHS.Type == null) | ||
{ | ||
Error(diagnostics, ErrorCode.ERR_DeconstructRequiresExpression, node); | ||
return BadExpression(node, new[] { boundRHS }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider using overload There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks. Done |
||
} | ||
|
||
return BindDeconstructWithDeconstruct(node, diagnostics, boundRHS, checkedVariables); | ||
} | ||
|
||
private BoundExpression BindDeconstructWithTuple(AssignmentExpressionSyntax node, DiagnosticBag diagnostics, BoundExpression boundRHS, ImmutableArray<BoundExpression> checkedVariables) | ||
{ | ||
// patch up the tuple literal so that it has a type, based on the LHS information | ||
var lhsTypes = checkedVariables.SelectAsArray(v => v.Type); | ||
TypeSymbol lhsAsTuple = TupleTypeSymbol.Create(locationOpt: null, elementTypes: lhsTypes, elementLocations: default(ImmutableArray<Location>), elementNames: default(ImmutableArray<string>), compilation: Compilation, diagnostics: diagnostics); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure. The left-hand-side really isn't a tuple (it's a set of individual variables), although it has tuple-looking syntax... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think locations should not be set here because this is not a declaration location for the tuple type. In reply to: 64080194 [](ancestors = 64080194) |
||
var typedRHS = GenerateConversionForAssignment(lhsAsTuple, boundRHS, diagnostics); | ||
|
||
ImmutableArray<TypeSymbol> tupleTypes = typedRHS.Type.TupleElementTypes; | ||
|
||
// figure out the pairwise conversions | ||
var assignmentsBuilder = ArrayBuilder<BoundDeconstructionAssignmentOperator.AssignmentInfo>.GetInstance(checkedVariables.Length); | ||
|
||
for (int i = 0; i < checkedVariables.Length; i++) | ||
{ | ||
var leftPlaceholder = new BoundLValuePlaceholder(checkedVariables[i].Syntax, checkedVariables[i].Type) { WasCompilerGenerated = true }; | ||
var rightPlaceholder = new BoundRValuePlaceholder(node.Right, tupleTypes[i]) { WasCompilerGenerated = true }; | ||
|
||
// each assignment has a placeholder for a receiver and another for the source | ||
BoundAssignmentOperator op = BindAssignment(node, leftPlaceholder, rightPlaceholder, diagnostics); | ||
assignmentsBuilder.Add(new BoundDeconstructionAssignmentOperator.AssignmentInfo() { Assignment = op, LValuePlaceholder = leftPlaceholder, RValuePlaceholder = rightPlaceholder }); | ||
} | ||
|
||
var assignments = assignmentsBuilder.ToImmutableAndFree(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The calculation of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes :-) |
||
|
||
TypeSymbol lastType = tupleTypes.Last(); | ||
return new BoundDeconstructionAssignmentOperator(node, checkedVariables, typedRHS, deconstructMemberOpt: null, assignments: assignments, type: lastType); | ||
} | ||
|
||
private BoundExpression BindDeconstructWithDeconstruct(AssignmentExpressionSyntax node, DiagnosticBag diagnostics, BoundExpression boundRHS, ImmutableArray<BoundExpression> checkedVariables) | ||
{ | ||
// symbol and parameters for Deconstruct | ||
DiagnosticBag bag = new DiagnosticBag(); | ||
MethodSymbol deconstructMethod = FindDeconstruct(checkedVariables, boundRHS, node, bag); | ||
|
@@ -1754,7 +1807,7 @@ private BoundExpression BindDeconstructionAssignment(AssignmentExpressionSyntax | |
} | ||
|
||
// figure out the pairwise conversions | ||
var assignmentsBuilder = ArrayBuilder<BoundDeconstructionAssignmentOperator.AssignmentInfo>.GetInstance(numElements); | ||
var assignmentsBuilder = ArrayBuilder<BoundDeconstructionAssignmentOperator.AssignmentInfo>.GetInstance(checkedVariables.Length); | ||
var deconstructParameters = deconstructMethod.Parameters; | ||
for (int i = 0; i < checkedVariables.Length; i++) | ||
{ | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,9 @@ | ||
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Immutable; | ||
using System.Diagnostics; | ||
using System.Linq; | ||
using Microsoft.CodeAnalysis.CSharp.Symbols; | ||
|
||
namespace Microsoft.CodeAnalysis.CSharp | ||
|
@@ -9,9 +12,6 @@ internal sealed partial class LocalRewriter | |
{ | ||
public override BoundNode VisitDeconstructionAssignmentOperator(BoundDeconstructionAssignmentOperator node) | ||
{ | ||
CSharpSyntaxNode syntax = node.Syntax; | ||
int numVariables = node.LeftVariables.Length; | ||
|
||
var temps = ArrayBuilder<LocalSymbol>.GetInstance(); | ||
var stores = ArrayBuilder<BoundExpression>.GetInstance(); | ||
|
||
|
@@ -24,40 +24,27 @@ public override BoundNode VisitDeconstructionAssignmentOperator(BoundDeconstruct | |
} | ||
|
||
BoundExpression loweredRight = VisitExpression(node.Right); | ||
ImmutableArray<BoundExpression> rhsValues; | ||
|
||
// prepare out parameters for Deconstruct | ||
var deconstructParameters = node.DeconstructMember.Parameters; | ||
var outParametersBuilder = ArrayBuilder<BoundExpression>.GetInstance(deconstructParameters.Length); | ||
Debug.Assert(deconstructParameters.Length == node.LeftVariables.Length); | ||
|
||
for (int i = 0; i < numVariables; i++) | ||
// get or make right-hand-side values | ||
if (node.Right.Type.IsTupleType) | ||
{ | ||
var localSymbol = new SynthesizedLocal(_factory.CurrentMethod, deconstructParameters[i].Type, SynthesizedLocalKind.LoweringTemp); | ||
|
||
var localBound = new BoundLocal(syntax, | ||
localSymbol, | ||
null, | ||
deconstructParameters[i].Type | ||
) { WasCompilerGenerated = true }; | ||
|
||
temps.Add(localSymbol); | ||
outParametersBuilder.Add(localBound); | ||
rhsValues = AccessTupleFields(node, loweredRight, temps, stores); | ||
} | ||
else | ||
{ | ||
rhsValues = CallDeconstruct(node, loweredRight, temps, stores); | ||
} | ||
|
||
var outParameters = outParametersBuilder.ToImmutableAndFree(); | ||
|
||
// invoke Deconstruct | ||
var invokeDeconstruct = MakeCall(syntax, loweredRight, node.DeconstructMember, outParameters, node.DeconstructMember.ReturnType); | ||
stores.Add(invokeDeconstruct); | ||
|
||
// assign from out temps to lhs receivers | ||
for (int i = 0; i < numVariables; i++) | ||
// assign from rhs values to lhs receivers | ||
int numAssignments = node.Assignments.Length; | ||
for (int i = 0; i < numAssignments; i++) | ||
{ | ||
// lower the assignment and replace the placeholders for source and target in the process | ||
var assignmentInfo = node.Assignments[i]; | ||
|
||
AddPlaceholderReplacement(assignmentInfo.LValuePlaceholder, lhsReceivers[i]); | ||
AddPlaceholderReplacement(assignmentInfo.RValuePlaceholder, outParameters[i]); | ||
AddPlaceholderReplacement(assignmentInfo.RValuePlaceholder, rhsValues[i]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like this loop interleaves LHS accesses and RHS reads. That does not seem right.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although the code above needs to be updated after LDM decision to not bundle conversion with assignment, I think it is otherwise correct and matches your description. All the LHS side-effects have already been stacked on line 23: The loop that you commented on only takes care of conversion+assigment (step 2 from your description). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I see. Then it should be all good. |
||
|
||
var assignment = VisitExpression(assignmentInfo.Assignment); | ||
|
||
|
@@ -75,5 +62,76 @@ public override BoundNode VisitDeconstructionAssignmentOperator(BoundDeconstruct | |
|
||
return result; | ||
} | ||
|
||
private ImmutableArray<BoundExpression> AccessTupleFields(BoundDeconstructionAssignmentOperator node, BoundExpression loweredRight, ArrayBuilder<LocalSymbol> temps, ArrayBuilder<BoundExpression> stores) | ||
{ | ||
var tupleType = loweredRight.Type.IsTupleType ? loweredRight.Type : TupleTypeSymbol.Create((NamedTypeSymbol)loweredRight.Type); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like this function is never called when IsTupleType is false. Consider simply asserting this instead. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. This is un-necessary now that the conversion is applied to all tuple cases. Thanks There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Turns out I was wrong. Tests won't pass without this logic. The key is that the node on the right may have a tuple type before lowering, but after lowering it becomes a tuple constructor call (which returns underlying type). |
||
var tupleElementTypes = tupleType.TupleElementTypes; | ||
|
||
var numElements = tupleElementTypes.Length; | ||
Debug.Assert(numElements == node.LeftVariables.Length); | ||
|
||
CSharpSyntaxNode syntax = node.Syntax; | ||
|
||
// save the loweredRight as we need to access it multiple times | ||
BoundAssignmentOperator assignmentToTemp; | ||
var savedTuple = _factory.StoreToTemp(loweredRight, out assignmentToTemp); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably optimize for a case when it is already a local or parameter. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Added a work item in #11299. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is in general not a trivial optimization. RHS could be captured in a closure and changed between reads, could be used on the left, could be aliased via a byref local... Simple redundant temp cases should be already handled in the codegen. |
||
stores.Add(assignmentToTemp); | ||
temps.Add(savedTuple.LocalSymbol); | ||
|
||
// list the tuple fields | ||
var fieldAccessorsBuilder = ArrayBuilder<BoundExpression>.GetInstance(numElements); | ||
|
||
for (int i = 0; i < numElements; i++) | ||
{ | ||
var fields = tupleType.GetMembers(TupleTypeSymbol.TupleMemberName(i + 1)).OfType<FieldSymbol>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is not a reliable way to lookup the field. We should probably add an API GetTupleElementField(int elementIndex). Let's talk offline in more details. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. I was a bit unsure about this, as I had to pick one of multiple members (although I never expect multiple). We used to have a way to enumerate field symbols on the tuple symbol, but I don't think it's there anymore. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
var field = fields.Single(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We cannot make this assumption. This is related to the previous comment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replaced with new method of getting fields. |
||
var fieldAccess = MakeTupleFieldAccess(syntax, field, savedTuple, null, LookupResultKind.Empty, tupleElementTypes[i]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Implementation of MakeTupleFieldAccess assumes use-site errors for the field are reported prior to calling it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't understand this comment. I'll stop by in the morning. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See what happens when underlying type doesn't define the field for the element. In reply to: 64172103 [](ancestors = 64172103) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added such test and fixed. Thanks |
||
fieldAccessorsBuilder.Add(fieldAccess); | ||
} | ||
|
||
return fieldAccessorsBuilder.ToImmutableAndFree(); | ||
} | ||
|
||
/// <summary> | ||
/// Prepares local variables to be used in Deconstruct call | ||
/// Adds a invocation of Deconstruct with those as out parameters onto the 'stores' sequence | ||
/// Returns the expressions for those out parameters | ||
/// </summary> | ||
private ImmutableArray<BoundExpression> CallDeconstruct(BoundDeconstructionAssignmentOperator node, BoundExpression loweredRight, ArrayBuilder<LocalSymbol> temps, ArrayBuilder<BoundExpression> stores) | ||
{ | ||
Debug.Assert((object)node.DeconstructMemberOpt != null); | ||
|
||
int numVariables = node.LeftVariables.Length; | ||
CSharpSyntaxNode syntax = node.Syntax; | ||
|
||
// prepare out parameters for Deconstruct | ||
var deconstructParameters = node.DeconstructMemberOpt.Parameters; | ||
var outParametersBuilder = ArrayBuilder<BoundExpression>.GetInstance(deconstructParameters.Length); | ||
Debug.Assert(deconstructParameters.Length == node.LeftVariables.Length); | ||
|
||
for (int i = 0; i < numVariables; i++) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
{ | ||
var localSymbol = new SynthesizedLocal(_factory.CurrentMethod, deconstructParameters[i].Type, SynthesizedLocalKind.LoweringTemp); | ||
|
||
var localBound = new BoundLocal(syntax, | ||
localSymbol, | ||
null, | ||
deconstructParameters[i].Type | ||
) | ||
{ WasCompilerGenerated = true }; | ||
|
||
temps.Add(localSymbol); | ||
outParametersBuilder.Add(localBound); | ||
} | ||
|
||
var outParameters = outParametersBuilder.ToImmutableAndFree(); | ||
|
||
// invoke Deconstruct | ||
var invokeDeconstruct = MakeCall(syntax, loweredRight, node.DeconstructMemberOpt, outParameters, node.DeconstructMemberOpt.ReturnType); | ||
stores.Add(invokeDeconstruct); | ||
|
||
return outParameters; | ||
} | ||
} | ||
} |
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 using
foreach
above.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.
Done.