-
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
Don't require ValueTuple type in deconstruction if return value is not used #20295
Conversation
@cston I think the solution you suggested mostly works, with some additional tweaks in lowering. But there is one issue left with EE. In CompilationContext.BindExpression we (1) bind the expression as part of an expression-statement, and then (2) bind the resulting bound value into a The problem is that step (1) will consider that the return value isn't used, but when the bound node from step (2) reaches the emit stage, the emitter sees that the value is used. I've tried a simple modification of step (1) to wrap the expression in a return-statement. But that won't work for expressions like Update: From discussion with Chuck, I will try modifying CompilationContext.BindExpression to handle deconstructions directly (recognize the case, call BindDeconstruction, passing a flag). Related tests:
|
@cston @dotnet/roslyn-compiler for review. Thanks |
return new BoundDeconstructionAssignmentOperator(node, lhsTuple, boundConversion, resultIsUsed, returnType); | ||
} | ||
|
||
private bool IsDeconstructionResultUsed(ExpressionSyntax left) |
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.
static
#Resolved
private bool IsDeconstructionResultUsed(ExpressionSyntax left) | ||
{ | ||
var parent = left.Parent; | ||
if (parent is null || left.Parent.Kind() == SyntaxKind.ForEachVariableStatement) |
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.
|| parent.Kind() == ...
#Resolved
@@ -1095,6 +1090,119 @@ public void Deconstruct(out int a, out int b) | |||
} | |||
|
|||
[Fact] | |||
public void ValueTupleNotRequiredIfReturnIsUsed() |
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.
Should this be ...IfReturnIsNotUsed
? #Resolved
@@ -116,9 +121,10 @@ private BoundExpression BindDeconstruction(AssignmentExpressionSyntax node, Diag | |||
var type = boundRHS.Type ?? voidType; | |||
return new BoundDeconstructionAssignmentOperator( | |||
node, | |||
DeconstructionVariablesAsTuple(node, checkedVariables, diagnostics, hasErrors: true), | |||
DeconstructionVariablesAsTuple(node, checkedVariables, resultIsUsed ? diagnostics : null, hasErrors: true), |
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.
Is there a test for this case, where boundRHS.Type.IsErrorType()
and resultIsUsed == false
? #Resolved
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.
Added one. I'll stop by to discuss. #Resolved
} | ||
|
||
[Fact] | ||
public void ValueTupleNotRequiredIfReturnIsUsed2() |
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.
...IfReturnIsNotUsed2
#Resolved
</Node> | ||
|
||
<Node Name="BoundVoid" Base="BoundExpression"> | ||
<!-- Non-null type is required for this node kind, but it will always be the void 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'll remove the comment. We'll be using a type that is not "void".
@cston I think this reinforces your discomfort with calling this node BoundVoid. I'll try to think of another name or approach. #Resolved
@@ -1095,6 +1090,119 @@ public void Deconstruct(out int a, out int b) | |||
} | |||
|
|||
[Fact] | |||
public void ValueTupleNotRequiredIfReturnIsNotUsed() |
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 adding [WorkItem(18629)]
. #Resolved
LGTM |
@@ -77,6 +77,8 @@ private BoundExpression BindDeconstruction(AssignmentExpressionSyntax node, Diag | |||
/// <param name="diagnostics">Where to report diagnostics</param> | |||
/// <param name="declaration">A variable set to the first variable declaration found in the left</param> | |||
/// <param name="expression">A variable set to the first expression in the left that isn't a declaration or discard</param> | |||
/// <param name="resultIsUsedOverride">The expression evaluator needs bind deconstructions (both assignments and declarations) as expression-statements |
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.
needs bind [](start = 72, length = 10)
needs to bind? #Closed
private static bool IsDeconstructionResultUsed(ExpressionSyntax left) | ||
{ | ||
var parent = left.Parent; | ||
if (parent is null || parent.Kind() == SyntaxKind.ForEachVariableStatement) |
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.
parent is null [](start = 16, length = 14)
Are we building with compiler that performs efficient code gen for this condition? #Closed
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.
Not yet. The master
branch is using 2.3.0-beta2-61716-09
(from May 16th), but this optimization went into on May 30th. We will definitely refresh with RTM drop (if not sooner), and this PR is for 15.6. #Resolved
@@ -116,9 +121,10 @@ private BoundExpression BindDeconstruction(AssignmentExpressionSyntax node, Diag | |||
var type = boundRHS.Type ?? voidType; | |||
return new BoundDeconstructionAssignmentOperator( | |||
node, | |||
DeconstructionVariablesAsTuple(node, checkedVariables, diagnostics, hasErrors: true), | |||
DeconstructionVariablesAsTuple(node, checkedVariables, resultIsUsed ? diagnostics : null, hasErrors: true), |
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.
DeconstructionVariablesAsTuple(node, checkedVariables, resultIsUsed ? diagnostics : null, hasErrors: true) [](start = 28, length = 106)
I am not comfortable with this change. It suppresses all diagnostics that could be reported by DeconstructionVariablesAsTuple function. It is very possible that might suppress to much in the future, or even does that already for some scenarios today. I think we should be very precise about what diagnostics we suppress and where. #Closed
} | ||
else | ||
{ | ||
returnValue = new BoundUnusedResult(left.Syntax, left.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.
left.Type [](start = 69, length = 9)
Is this possibly an error type? This doesn't feel right, we probably shouldn't have any references to error types after lowering. #Closed
Why even bother doing all this if we don't need the result? #Closed Refers to: src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_DeconstructionAssignmentOperator.cs:111 in 77068fa. [](commit_id = 77068fae0aa9fed6a55cedf0ac8a9f7358152e7a, deletion_comment = False) |
@@ -109,7 +109,7 @@ internal sealed class TupleTypeSymbol : WrappedNamedTypeSymbol | |||
} | |||
|
|||
var constructedType = Create(underlyingType, elementNames, errorPositions, locationOpt, elementLocations); | |||
if (shouldCheckConstraints) | |||
if (shouldCheckConstraints && diagnostics != null) |
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.
&& diagnostics != null [](start = 38, length = 23)
This doesn't feel right, we might be suppressing errors that have nothing to do with availability of the type. #Closed
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 guess it is fine to suppress constraints checks as well when the type is not going to be used at the end.
In reply to: 123330421 [](ancestors = 123330421)
@@ -1095,6 +1090,122 @@ public void Deconstruct(out int a, out int b) | |||
} | |||
|
|||
[Fact] | |||
[WorkItem(18629, "https://github.com/dotnet/roslyn/issues/18629")] | |||
public void ValueTupleNotRequiredIfReturnIsNotUsed() |
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.
ValueTupleNotRequiredIfReturnIsNotUsed [](start = 20, length = 38)
Should probably have tests to verify that obsolete diagnostics is suppressed even when the type is present. #Closed
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.
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 see that we already have tests covering constraints.
In reply to: 123337083 [](ancestors = 123337083,123336564)
@@ -1228,21 +1339,6 @@ public void Deconstruct(out ArgIterator a, out int b) | |||
// (14,46): error CS0306: The type 'ArgIterator' may not be used as a type argument | |||
// public static (ArgIterator, ArgIterator) M2() | |||
Diagnostic(ErrorCode.ERR_BadTypeArgument, "M2").WithArguments("System.ArgIterator").WithLocation(14, 46), | |||
// (7,22): error CS0306: The type 'ArgIterator' may not be used as a type argument | |||
// (int x, var (err1, y)) = (0, new C()); | |||
Diagnostic(ErrorCode.ERR_BadTypeArgument, "err1").WithArguments("System.ArgIterator").WithLocation(7, 22), |
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.
Diagnostic(ErrorCode.ERR_BadTypeArgument, "err1").WithArguments("System.ArgIterator").WithLocation(7, 22), [](start = 16, length = 106)
Do we have other tests covering this diagnostics? #Closed
Done with review pass. #Closed |
@cston from discussion with aleksey, I changed to use the last element of the tuple return value as the return value. #Resolved |
{ | ||
returnTuple = VisitExpression(returnTuple); | ||
// When a deconstruction is not used, we use the last effect is used as return value |
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.
we use the last effect is used as return value [](start = 54, length = 46)
Looks like there is a typo "we use the last effect is used as return value" #Closed
} | ||
else | ||
{ | ||
if (!returnValue.HasErrors && isUsed) |
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.
&& isUsed [](start = 42, length = 10)
It looks like the check for isUsed
is redundant, the outer if
checked it already. #Closed
if (last is null) | ||
{ | ||
// Deconstructions with no effects lower to nothing. For example, `(_, _) = (1, 2);` | ||
result = null; |
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.
result = null; [](start = 20, length = 14)
Are we leaking temps
and effects
when we take this code path? #Closed
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.
Yes :-( Thanks
{ | ||
returnTuple = VisitExpression(returnTuple); | ||
// When a deconstruction is not used, we use the last effect is used as return value | ||
var last = effects.PopLast(); |
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.
var last = effects.PopLast(); [](start = 16, length = 29)
Consider asserting that returnValue
is null. #Closed
@cston Could you take another look? |
@@ -467,7 +504,8 @@ private static TypeSymbol MakeMergedTupleType(ArrayBuilder<DeconstructionVariabl | |||
errorPositions: default(ImmutableArray<bool>)); | |||
} | |||
|
|||
private BoundTupleLiteral DeconstructionVariablesAsTuple(CSharpSyntaxNode syntax, ArrayBuilder<DeconstructionVariable> variables, DiagnosticBag diagnostics, bool hasErrors) | |||
private BoundTupleLiteral DeconstructionVariablesAsTuple(CSharpSyntaxNode syntax, ArrayBuilder<DeconstructionVariable> variables, | |||
DiagnosticBag diagnostics, bool ignoreDiagnosticsFromTuple) |
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.
Is ignoreDiagnosticsFromTuple
needed or could the caller pass diagnostics: null
in that case?
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.
We could pass null
diagnostics, but the feedback from Aleksey was that pushing that logic down (being more explicit) is safer as the code changes in the future.
return last; | ||
} | ||
return null; | ||
} |
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.
There are five separate cases in PopLast
. Are we testing all of these? Alternatively, would this be simpler if ToImmutableAndFree()
is split into separate GetExpressions
and Free
methods, and the caller handles getting the last expression?
void GetExpressions(ArrayBuilder<Expression> builder)
{
builder.AddRange(init);
builder.AddRange(deconstructions);
builder.AddRange(conversions);
builder.AddRange(assignments);
}
In the caller:
var builder = ArrayBuilder<Expression>.GetInstance();
effects.GetExpressions(builder);
effects.Free();
if (isUsed)
{
result = _factory._factory.Sequence(temps.ToImmutableAndFree(), builder.ToImmutable(), returnValue);
}
else if (builder.Count == 0)
{
result = null;
}
else
{
var last = builder.Last();
builder.RemoveLast();
result = _factory._factory.Sequence(temps.ToImmutableAndFree(), builder.ToImmutable(), last);
}
builder.Free();
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.
LGTM, assuming tests are passing.
@cston Any other feedback? |
No additional feedback other than simplifying |
LGTM |
VS integration timeout (details) I'll re-run
|
test windows_debug_vs-integration_prtest please |
test windows_release_vs-integration_prtest please |
test windows_debug_vs-integration_prtest please |
test timeout in VS integration. FYI @dotnet/roslyn-infrastructure (details) I'll re-run
|
test windows_debug_vs-integration_prtest please |
test windows_debug_vs-integration_prtest please |
Deconstructions without tuples in the right-hand-side, such as
(x, y) = e;
, shouldn't require the ValueTuple types, because the only tuple involved is the return value.This change has two parts: (1) binding, and (2) lowering.
When binding a deconstruction, we can ignore the errors that come with the return type, when the return value is not used. We know the return value is not used in an expression-statement, and the initializer and incrementor parts of a for-statement.
When lowering the deconstruction we ignore the return value. It does not get lowered itself and gets replaced by a
BoundVoid
for emit stage.This maintains the behavior of the semantic model: the type of the whole deconstruction expression is still a tuple type. But its underlying type may be an error type if the ValueTuple types were missing.
Fixes #18629