-
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
Deconstruction-assignment for tuples #11457
Changes from 6 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 |
---|---|---|
|
@@ -36,17 +36,18 @@ In short, what this does is find a `Deconstruct` method on the expression on the | |
The existing assignment binding currently checks if the variable on its left-hand-side can be assigned to and if the two sides are compatible. | ||
It will be updated to support deconstruction-assignment, ie. when the left-hand-side is a tuple-literal/tuple-expression: | ||
|
||
- Each item on the left needs to be assignable and needs to be compatible with corresponding position on the right | ||
- Needs to break the right-hand-side into items. That step is un-necessary if the right-hand-side is already a tuple though. | ||
- Each item on the left needs to be assignable and needs to be compatible with corresponding position on the right (resulting from previous step). | ||
- Needs to handle nesting case such as `(x, (y, z)) = M();`, but note that the second item in the top-level group has no discernable type. | ||
|
||
The lowering for deconstruction-assignment would translate: `(expressionX, expressionY, expressionZ) = (expressionA, expressionB, expressionC)` into: | ||
In the general case, the lowering for deconstruction-assignment would translate: `(expressionX, expressionY, expressionZ) = expressionRight` into: | ||
|
||
``` | ||
tempX = &evaluate expressionX | ||
tempY = &evaluate expressionY | ||
tempZ = &evaluate expressionZ | ||
|
||
tempRight = evaluate right and evaluate Deconstruct | ||
tempRight = evaluate right and evaluate Deconstruct in three parts | ||
|
||
tempX = tempRight.A (including conversions) | ||
tempY = tempRight.B (including conversions) | ||
|
@@ -80,23 +81,16 @@ evaluate side-effect on the left-hand-side variables | |
evaluate Deconstruct passing the references directly in | ||
``` | ||
|
||
Note that the feature is built around the `Deconstruct` mechanism for deconstructing types. | ||
`ValueTuple` and `System.Tuple` will rely on that same mechanism, except that the compiler may need to synthesize the proper `Deconstruct` methods. | ||
|
||
In the case where the expression on the right is a tuple, the evaluation order becomes: | ||
``` | ||
evaluate side-effect on the left-hand-side variables | ||
evaluate the right-hand-side and do a tuple conversion | ||
assign element-wise from the right to the left | ||
``` | ||
|
||
**Work items, open issues and assumptions to confirm with LDM:** | ||
Note that tuples (`System.ValueTuple`) don't need to invoke Deconstruct. | ||
But `System.Tuple` will rely on the Deconstruct mechanism. | ||
|
||
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. Per LDM, I believe System.Tuple will get special handling that does not depend on Deconstruct (i.e. for long System.Tuple types) 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, my work items list tracks "System.Tuple deconstruction". I'll update the spec then. 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've made some update the the |
||
- I assume this should work even if `System.ValueTuple` is not present. | ||
- How is the Deconstruct method resolved? | ||
- I assumed there can be no ambiguity. Only one `Deconstruct` is allowed (in nesting cases we have no type to guide the resolution process). | ||
- But we may allow a little bit of ambiguity and preferring an instance over extension method. | ||
- Do the names matter? `int x, y; (a: x, b: y) = M();` | ||
- Can we deconstruct into a single out variable? I assume no. | ||
- I assume no compound assignment `(x, y) += M();` | ||
- [ ] Provide more details on the semantic of deconstruction-assignment, both static (The LHS of the an assignment-expression used be a L-value, but now it can be L-value -- which uses existing rules -- or tuple_literal. The new rules for tuple_literal on the LHS...) and dynamic. | ||
- [ ] Discuss with Aleksey about "Deconstruct and flow analysis for nullable ref type" | ||
- [ ] Validate any target typing or type inference scenarios. | ||
- The deconstruction-assignment is treated separately from deconstruction-declaration, which means it doesn't allow combinations such as `int x; (x, int y) = M();`. | ||
|
||
###Deconstruction-declaration (deconstruction into new variables): | ||
|
||
|
@@ -158,20 +152,11 @@ constant_declarator // not sure | |
; | ||
``` | ||
|
||
Treat deconstruction of a tuple into new variables as a new kind of node (AssignmentExpression). | ||
Treat deconstruction of a tuple into new variables as a new kind of node (not AssignmentExpression). | ||
It would pick up the behavior of each contexts where new variables can be declared (TODO: need to list). For instance, in LINQ, new variables go into a transparent identifiers. | ||
It is seen as deconstructing into separate variables (we don't introduce transparent identifiers in contexts where they didn't exist previously). | ||
|
||
Should we allow this? | ||
`var t = (x: 1, y: 2); (x: var a, y: var b) = t;` | ||
or `var (x: a, y: b) = t;` | ||
(if not, tuple names aren't very useful?) | ||
|
||
- [ ] Add example: var (x, y) = | ||
- [ ] Semantic (cardinality should match, ordering including conversion, | ||
- [ ] What are the type rules? `(string s, int x) = (null, 3);` | ||
|
||
- Deconstruction for `System.ValueTuple`, `System.Tuple` and any other type involves a call to `Deconstruct`. | ||
Just like deconstruction-assignment, deconstruction-declaration does not need to invoke `Deconstruct` for tuple types. | ||
|
||
**References** | ||
|
||
|
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; | ||
|
@@ -1727,16 +1734,50 @@ private BoundExpression BindDeconstructionAssignment(AssignmentExpressionSyntax | |
|
||
// bind the variables and check they can be assigned to | ||
var checkedVariablesBuilder = ArrayBuilder<BoundExpression>.GetInstance(numElements); | ||
for (int i = 0; i < numElements; i++) | ||
foreach (var argument in arguments) | ||
{ | ||
var boundVariable = BindExpression(arguments[i].Expression, diagnostics, invoked: false, indexed: false); | ||
var boundVariable = BindExpression(argument.Expression, diagnostics, invoked: false, indexed: false); | ||
var checkedVariable = CheckValue(boundVariable, BindValueKind.Assignment, diagnostics); | ||
|
||
checkedVariablesBuilder.Add(checkedVariable); | ||
} | ||
|
||
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 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 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, boundRHS); | ||
} | ||
|
||
return BindDeconstructWithDeconstruct(node, diagnostics, boundRHS, checkedVariables); | ||
} | ||
|
||
private BoundExpression BindDeconstructWithTuple(AssignmentExpressionSyntax node, DiagnosticBag diagnostics, BoundExpression boundRHS, ImmutableArray<BoundExpression> checkedVariables) | ||
{ | ||
// make a conversion for the tuple based on the LHS information (this also takes care of tuple literals without a natural type) | ||
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 assignments = checkedVariables.SelectAsArray((variable, index, types) => MakeAssignmentInfo(variable, types[index], node, diagnostics), tupleTypes); | ||
|
||
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,24 +1795,27 @@ private BoundExpression BindDeconstructionAssignment(AssignmentExpressionSyntax | |
} | ||
|
||
// figure out the pairwise conversions | ||
var assignmentsBuilder = ArrayBuilder<BoundDeconstructionAssignmentOperator.AssignmentInfo>.GetInstance(numElements); | ||
var deconstructParameters = deconstructMethod.Parameters; | ||
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, deconstructParameters[i].Type) { 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(); | ||
var assignments = checkedVariables.SelectAsArray((variable, index, parameters) => MakeAssignmentInfo(variable, parameters[index].Type, node, diagnostics), deconstructParameters); | ||
|
||
TypeSymbol lastType = deconstructParameters.Last().Type; | ||
return new BoundDeconstructionAssignmentOperator(node, checkedVariables, boundRHS, deconstructMethod, assignments, lastType); | ||
} | ||
|
||
/// <summary> | ||
/// Figures out how to assign from sourceType into receivingVariable and bundles the information (leaving holes for the actual source and receiver) into an AssignmentInfo. | ||
/// </summary> | ||
private BoundDeconstructionAssignmentOperator.AssignmentInfo MakeAssignmentInfo(BoundExpression receivingVariable, TypeSymbol sourceType, AssignmentExpressionSyntax node, DiagnosticBag diagnostics) | ||
{ | ||
var leftPlaceholder = new BoundLValuePlaceholder(receivingVariable.Syntax, receivingVariable.Type) { WasCompilerGenerated = true }; | ||
var rightPlaceholder = new BoundRValuePlaceholder(node.Right, sourceType) { WasCompilerGenerated = true }; | ||
|
||
// each assignment has a placeholder for a receiver and another for the source | ||
BoundAssignmentOperator op = BindAssignment(node, leftPlaceholder, rightPlaceholder, diagnostics); | ||
|
||
return new BoundDeconstructionAssignmentOperator.AssignmentInfo() { Assignment = op, LValuePlaceholder = leftPlaceholder, RValuePlaceholder = rightPlaceholder }; | ||
} | ||
|
||
/// <summary> | ||
/// Find the Deconstruct method for the expression on the right, that will fit the assignable bound expressions on the left. | ||
/// Returns true if the Deconstruct method is found. | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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.
tuple conversion to what type?
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 basically make a fake tuple type that looks like what is on the LHS. I'm not sure what is a good way to explain that in the spec though.