-
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
Lookup for Deconstruct method #11873
Conversation
|
||
BoundExpression result = BindInvocationExpression( | ||
receiverSyntax, receiverSyntax, methodName, boundExpression, analyzedArguments, diagnostics, queryClause: null, | ||
allowUnexpandedForm: 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.
Will this bind successfully to a field: dynamic Deconstruct;
? Is that allowed?
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 added two tests: one with an object Deconstruct
field (which properly reports that no invocable Deconstruct method was found), and one with dynamic Deconstruct
field (as you suggested).
This latter one crashes. I'll investigate and fix.
Thanks for the suggestion!
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 added a check that the member we're trying to use is a MethodGroup
before we proceed with its invocation.
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.
If we're checking for MethodGroup
consider calling BindMethodGroupInvocation
directly instead of BindInvocationExpression
.
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 (calling BindMethodGroupInvocation
directly).
f030603
to
cc63d93
Compare
[Fact] | ||
public void DeconstructUsingDynamicMethod() | ||
{ | ||
string source = @" |
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.
but Deconstruct can have dynamic parameters, right? - Deconstruct(out dynamic arg1)
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. The "Dynamic" test covers the scenario (dynamic types on the left and as output parameters):
class C
{
dynamic Dynamic1;
dynamic Dynamic2;
static void Main()
{
C c = new C();
(c.Dynamic1, c.Dynamic2) = c;
System.Console.WriteLine(c.Dynamic1 + "" "" + c.Dynamic2);
}
public void Deconstruct(out int a, out dynamic b)
{
a = 1;
b = ""hello"";
}
}
Some more test scenarios to try:
public delegate void D1(out int x, out int y);
class c1
{
public D1 Deconstruct;
}
public delegate void D1(out int x, out int y);
class c1
{
public event D1 Deconstruct;
static void Test()
{
var c = new c1();
int x;
// does this work?
(x, x) = c;
}
}
|
|
||
public BoundDeconstructValuePlaceholder FailInference(Binder binder) | ||
{ | ||
return SetInferredType(binder.CreateErrorType("var"), success: false); |
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.
When there is no obvious type name, other callers use CreateErrorType()
with the default name
argument 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.
Done. Thanks
@VSadov The latest commit adds the tests you suggested (except for VB one, which I recorded as work item). The results:
|
@@ -924,7 +924,7 @@ private static bool HadLambdaConversionError(DiagnosticBag diagnostics, Analyzed | |||
// If the expression is untyped because it is a lambda, anonymous method, method group or null | |||
// then we never want to report the error "you need a ref on that thing". Rather, we want to | |||
// say that you can't convert "null" to "ref int". | |||
if (!argument.HasExpressionType()) | |||
if (!argument.HasExpressionType() && argument.Kind != BoundKind.OutDeconstructVarPendingInference) |
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 the out var has no 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.
Yes, for instance "DeconstructWithInParam" reaches this code and the second argument has no type.
static void Main()
{
int x;
int y;
(x, y) = new C();
}
public void Deconstruct(out int x, int y) { x = 1; }
The overload resolution code generates this error, a few lines below:
// (8,9): error CS1615: Argument 2 may not be passed with the 'out' keyword
740ba17
to
0d86cc0
Compare
|
||
var deconstructMethod = ((BoundCall)result).Method; | ||
var parameters = deconstructMethod.IsExtensionMethod ? deconstructMethod.Parameters.Skip(1) : deconstructMethod.Parameters; | ||
if (parameters.Any(p => p.RefKind != RefKind.Out && !p.IsThis)) |
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.
Since you are skipping the first parameter for extension methods, do you need to check "IsThis" ?
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.
It might be easier to just do a small for loop here and start with 1 in case of extension methods. That would also avoid allocation since skip allocates. That is not a big deal though.
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, I'll remove IsThis
. It did not do what I expected (ie. returning true on the first parameter of the extension method) anyways. I'll change to a loop as you suggested too.
LGTM |
@jcouv - I am not sure if unacceptable deconstruct methods should shadow correct ones from the base. We need to check that with LDM, maybe. I.E. if derived has |
|
||
The resolution is equivalent to typing `rhs.Deconstruct(out var x1, out var x2, ...);` with the appropriate number of parameters to deconstruct into. | ||
It is based on normal overload resolution. | ||
This implies that `rhs` cannot be dynamic. |
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 am not sure if the Out Var limitation should necessarily translate into limitation for Deconstruction. I think it would be reasonable to treat all the output values as dynamic if the rhs is dynamic. #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.
I'll take a note for LDM on the dynamic question.
{ | ||
public BoundDeconstructValuePlaceholder Placeholder; | ||
|
||
public BoundDeconstructValuePlaceholder SetInferredType(TypeSymbol type, bool success) |
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.
This is implementing an essentially mutable bound node, which violates one of the design principles of the bound nodes. Why can this happen only once? What happens during overload resolution?
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.
Overload resolution doesn't mutate them. Coercion of the arguments does, which is done once after target method is known.
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.
And we do have other examples of mutable bound nodes, UnboundLambda, for example. So, the principle is not that sacred, but we do prefer to follow it, unless there are good reasons not to.
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.
UnboundLambda's mutation is just mutation of a cache. It acts the same no matter what order entries are placed in the cache.
In reply to: 66838259 [](ancestors = 66838259)
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.
Coercion of the arguments should create new nodes, not modify existing nodes.
In reply to: 66837824 [](ancestors = 66837824)
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.
It replaces the nodes in the tree, the original nodes (the ones that are mutated are removed from the tree). And the property that is mutated is a cache.
LGTM, but could you please add a test with use-site error scenario? |
Please revise the bound tree to not be mutable, and I'll continue reviewing then. |
get | ||
{ | ||
return MessageID.IDS_FeatureTuples.Localize(); | ||
} |
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 don't understand how this could make sense. Do you have a test for it?
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.
This is the same as #11493. I did not investigate in much details.
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.
This code is not meant to be reachable.
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.
More info: OverloadResolutionResult.ReportDiagnostics
checks that all the arguments have a non-null Display
.
// Each argument must have non-null Display in case it is used in a diagnostic.
Debug.Assert(arguments.Arguments.All(a => a.Display != null));
I'd be fine with Display
returning an empty string for this out var node.
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.
Fine with me, if you change it here, I'll make similar change for Out Vars.
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.
Changed to return string.Empty
@gafter, I don't see a reasonable way to avoid mutating the out var bound nodes. The options I see and that we discussed are:
Both seem less desirable than the current solution. Also, note that in the current solution coercion does indeed return new nodes. |
@jcouv When you bind something for the purpose of getting information about it, you should expect to look at the result of that binding to see what you got. In this case it means essentially looking at the arguments of the invocation. Each should be a direct placeholder, so no fishing should be needed. |
@gafter I think it is not constructive and counter-productive to stop your review process in ultimatum form just because you run int something you disagree with. There are plenty other changes in this PR that could benefit from more eyes. We can continue discussing that particular issue. |
LGTM |
7ab1ab4
to
3496abb
Compare
Latest commit addresses the this morning's feedback. I'll go ahead with checkin once CI is green. |
@@ -1735,6 +1735,12 @@ public override BoundNode VisitDeconstructionAssignmentOperator(BoundDeconstruct | |||
return null; | |||
} | |||
|
|||
public override BoundNode VisitOutDeconstructVarPendingInference(OutDeconstructVarPendingInference node) |
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 think this should go into PreciseAbstractFlowPass and be sealed. Also, should we have the same override in the LocalRewriter?
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. Thanks
LGTM |
Previously the finding of the Deconstruct method was very simple (find a member called "Deconstruct" and with the appropriate number of parameters).
This PR implements a full resolution, including base classes and extension methods.
To do this, we basically generate an invocation that looks like
rhs.Deconstruct(out var x1, out var x2, ...)
. So the change is modeled after Aleksey's recentout vars
change.The trick here is that the invocation receives a new kind of
out var
bound node, calledOutDeconstructVarPendingInference
. When the overload resolution is complete, those get replaced with placeholder of the inferred type.Those placeholders are collected into a
BoundDeconstructionDeconstructStep
as before, so that they can be replaced during lowering. This deconstruct step now holds the invocation expression generated above instead of just theMethodSymbol
for Deconstruct.The reason for not simply re-using the bound node introduced for
out var
(OutVarLocalPendingInference
, which is in the features/outvar branch at the moment) because that one generates a local after the resolution completes.CC @dotnet/roslyn-compiler for review.