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

Deconstruction-assignment for tuples #11457

Merged
merged 10 commits into from
May 24, 2016
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
43 changes: 14 additions & 29 deletions docs/features/deconstruction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Copy link
Member

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?

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 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.

```

**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.

Copy link
Member

Choose a reason for hiding this comment

The 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)

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

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've made some update the the deconstruction.md which reflect latest LDM, but are ahead of the implementation at this point.

- 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):

Expand Down Expand Up @@ -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**

Expand Down
72 changes: 58 additions & 14 deletions src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

Copy link
Member

Choose a reason for hiding this comment

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

Consider using foreach above.

Copy link
Member Author

Choose a reason for hiding this comment

The 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))
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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 (null, null).

{
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);
Copy link
Member

Choose a reason for hiding this comment

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

Should elementLocations be set from checkedVariables?

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 not sure. The left-hand-side really isn't a tuple (it's a set of individual variables), although it has tuple-looking syntax...
My goal here was to do the minimum faking of the left-hand-side-as-a-tuple so that I can give the expression on the right a proper type (the conversion logic).

Copy link
Contributor

Choose a reason for hiding this comment

The 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);
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@
<Field Name="LeftVariables" Type="ImmutableArray&lt;BoundExpression&gt;"/>

<Field Name="Right" Type="BoundExpression"/>
<Field Name="DeconstructMember" Type="MethodSymbol" Null="disallow"/>
<Field Name="DeconstructMemberOpt" Type="MethodSymbol" Null="allow"/>

<!-- The assignments have placeholders for the left and right part of the assignment -->
<Field Name="Assignments" Type="ImmutableArray&lt;BoundDeconstructionAssignmentOperator.AssignmentInfo&gt;" Null="NotApplicable" SkipInVisitor="true"/>
Expand Down
9 changes: 9 additions & 0 deletions src/Compilers/CSharp/Portable/CSharpResources.Designer.cs

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

3 changes: 3 additions & 0 deletions src/Compilers/CSharp/Portable/CSharpResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -4884,4 +4884,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<data name="ERR_MissingDeconstruct" xml:space="preserve">
<value>No Deconstruct instance or extension method was found for type '{0}'.</value>
</data>
<data name="ERR_DeconstructRequiresExpression" xml:space="preserve">
<value>Deconstruct assignment requires an expression with a type on the right-hand-side.</value>
</data>
</root>
1 change: 1 addition & 0 deletions src/Compilers/CSharp/Portable/Errors/ErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1395,5 +1395,6 @@ internal enum ErrorCode
ERR_AmbiguousDeconstruct = 8207,
ERR_DeconstructRequiresOutParams = 8208,
ERR_DeconstructWrongParams = 8209,
ERR_DeconstructRequiresExpression = 8210,
}
}
Loading