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

Lookup for Deconstruct method #11873

Merged
merged 11 commits into from
Jun 13, 2016
Merged

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Jun 8, 2016

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 recent out vars change.

The trick here is that the invocation receives a new kind of out var bound node, called OutDeconstructVarPendingInference. 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 the MethodSymbol 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.


BoundExpression result = BindInvocationExpression(
receiverSyntax, receiverSyntax, methodName, boundExpression, analyzedArguments, diagnostics, queryClause: null,
allowUnexpandedForm: true);
Copy link
Member

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?

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

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 added a check that the member we're trying to use is a MethodGroup before we proceed with its invocation.

Copy link
Member

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.

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 (calling BindMethodGroupInvocation directly).

[Fact]
public void DeconstructUsingDynamicMethod()
{
string source = @"
Copy link
Member

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)

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. 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"";
    }
}

@VSadov
Copy link
Member

VSadov commented Jun 10, 2016

Some more test scenarios to try:

  • void Deconstruct(out int x, out int y, int z = 42) // is this an operator?
  • void Deconstruct(out int x, out int y, params int[] z) // is this an operator?
  • void Deconstruct(out int x, out int y, __arglist) // is this an operator?
  • Is this an operator?
    public delegate void D1(out int x, out int y);

    class c1
    {
        public D1 Deconstruct;
    }
  • is this an operator:
    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;
        }
    }
  • in the above examples, assuming they are not operators, if base has normal void Deconstruct(out int x, out int y), does it work?
  • try defining Deconstruct in VB (I think you need to use [Out] attribute), low priority. It should work in theory


public BoundDeconstructValuePlaceholder FailInference(Binder binder)
{
return SetInferredType(binder.CreateErrorType("var"), success: false);
Copy link
Member

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.

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

@jcouv
Copy link
Member Author

jcouv commented Jun 10, 2016

@VSadov The latest commit adds the tests you suggested (except for VB one, which I recorded as work item).

The results:

  • Deconstruct with optional parameter and params parameter is resolved.
  • But not Deconstruct delegate or event or method with __arglist. In the __arglist case, you can have a proper Deconstruct next to it, and it will get picked up.

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

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

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


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

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

Copy link
Member

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.

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

@VSadov
Copy link
Member

VSadov commented Jun 11, 2016

LGTM

@VSadov
Copy link
Member

VSadov commented Jun 11, 2016

@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 Deconstruct(out int x, out int y, int x = 1) while base has Deconstruct(out int x, out int y). Should we ignore the derived one and pick the base one or should we fail?


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.
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 12, 2016

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

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'll take a note for LDM on the dynamic question.

{
public BoundDeconstructValuePlaceholder Placeholder;

public BoundDeconstructValuePlaceholder SetInferredType(TypeSymbol type, bool success)
Copy link
Member

@gafter gafter Jun 13, 2016

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?

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Member

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)

Copy link
Member

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)

Copy link
Contributor

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.

@AlekseyTs
Copy link
Contributor

LGTM, but could you please add a test with use-site error scenario?

@gafter
Copy link
Member

gafter commented Jun 13, 2016

Please revise the bound tree to not be mutable, and I'll continue reviewing then.

get
{
return MessageID.IDS_FeatureTuples.Localize();
}
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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

@jcouv
Copy link
Member Author

jcouv commented Jun 13, 2016

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

  • extensive re-writing of overload resolution
  • fishing for the placeholders in the tree returned by BindMethodGroupInvocation

Both seem less desirable than the current solution.

Also, note that in the current solution coercion does indeed return new nodes.

@gafter
Copy link
Member

gafter commented Jun 13, 2016

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

@AlekseyTs
Copy link
Contributor

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

@VSadov
Copy link
Member

VSadov commented Jun 13, 2016

LGTM

@jcouv
Copy link
Member Author

jcouv commented Jun 13, 2016

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)
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 this should go into PreciseAbstractFlowPass and be sealed. Also, should we have the same override in the LocalRewriter?

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

@AlekseyTs
Copy link
Contributor

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants