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

Support target-typed new #25196

Merged

Conversation

alrz
Copy link
Member

@alrz alrz commented Mar 2, 2018

@alrz alrz requested a review from a team as a code owner March 2, 2018 20:52
{
// TODO(target-typed-new): Use uncommon data to pass over the already computed
// TODO(target-typed-new): overload resolution results from succeeded conversion
// TODO(target-typed-new): to manually populate a BoundObjectCreationExpression
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 held this off until the design is finalized. There is certainly room for improvements i.e. we can avoid some overload resolution pass in some codepathes. I guess we should forbid dynamic arguments for now. When that's confirmed I'll refactor to eliminate redundant analysis and reuse precomputed overload resolution results.

@jcouv jcouv added this to the 16.0 milestone Mar 2, 2018
@jcouv
Copy link
Member

jcouv commented Mar 2, 2018

Thanks. Marked as blocked until LDM reviews the language design proposal. #Closed

@jcouv jcouv added the Community The pull request was submitted by a contributor who is not a Microsoft employee. label Mar 2, 2018
@alrz alrz force-pushed the features/target-typed-new branch from 4c1b011 to 54cc99c Compare March 2, 2018 21:03
@jcouv jcouv added the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Apr 4, 2018
@jcouv
Copy link
Member

jcouv commented Apr 4, 2018

Marking as "for personal review" for now, so it doesn't show in our "active reviews" query.
LDM is out of session for next 4 weeks or so.
I've added a note for this issue in https://github.com/dotnet/roslyn/projects/27 to remind me to push this onto the agenda once LDM restarts. #Closed

@jcouv jcouv removed Blocked PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. labels May 21, 2018
@jcouv
Copy link
Member

jcouv commented May 21, 2018

@alrz Sorry for the delay. LDM discussed this today and supports the feature (as usual, modulo unforseen major issues we might discover later). 🎉

As a result, we can start the review process for the PR. It would probably be good to refresh/rebase as a starting point.

We discussed the overload resolution rules and compat implications (see upcoming notes).
In short, an argument that is a target-typed new behaves like an out var in that it doesn't contribute a type to overload resolution. That behavior mitigates some of the compat implications of the feature.

We didn't get to cover other open issues, but I think we can get them on LDM agenda soon. Let's document them in the championed issue.
#Closed

@CyrusNajmabadi
Copy link
Member

We discussed the overload resolution rules and compat implications (see upcoming notes).
In short, an argument that is a target-typed new behaves like an out var in that it doesn't contribute a type to overload resolution. That behavior mitigates some of the compat implications of the feature.

Interesting! Though somewhat a pity as it means if you have overloads that are otherwise identical you'll have an ambiguity. But that's likely a good place to start from . In the future, it seems like actual rules for deciding how the new call could be part of overload resolution could be added solely to solve those ambiguous issues in a non-breaking fashion.

@Neme12
Copy link
Contributor

Neme12 commented May 21, 2018

Though somewhat a pity as it means if you have overloads that are otherwise identical you'll have an ambiguity.

Why? There are already perfectly good ways to resolve an ambiguity, the best one being just specifying the type of what you're creating. It's not clear at the call point what it is anyway. Overloading based on the arguments to new() seems really like a bad idea. Overload resolution is already complicated as it is. Why open a whole new can of worms?

Isn't the main scenario for target typed new just:

private readonly Dictionary<(string a, blah b), IEnumerable<Dictionary<string, (int blah, blahlabh b)>))> d = new();

?

I don't see how sprinkling new() all over method calls especially considering how extensively C# uses overload resolution is a good idea or makes the code clearer.

Though somewhat a pity

Can you give an example of code where this would be worth pitying over?

@Neme12
Copy link
Contributor

Neme12 commented May 21, 2018

I'm sorry, that's just how I see it. Please give me an example of where new() contributing to overload resolution would be useful and not make the code unreadable and error-prone.

I thought this would work just like default - as a shorthand when it's absolutely clear what the target type is. If there's any sort of ambiguity, just specify the type you need. Or if you prefer, use named argument. I don't see any problem with that.

@jaredpar
Copy link
Member

I'm sorry, that's just how I see it. Please give me an example of where new() contributing to overload resolution would be useful and not make the code unreadable and error-prone.

Consider this scenario:

// Util.dll 
public class C1 { } 
public class C2 {
  public C2(int count) { } 
}
public class C3 {
  public static M(C1 p) { } 
  public static M(C2 p) { }
}

Now consider I consume the code from a different library in the following manner:

C3.M(new (1));

So far so good. If new contributed to type inferencee then this would compile fine and everything would be great. Now imagine time passes and the author of Util.dll adds the following constructor:

public C1(int i) { } 

Now suddenly the consume is broken because new (1) is ambiguous between C1 and C2. Expanding this out it essentially means that evyr new constructur is potentially a source breaking change in the face of target typed new. This is the core scenario we're trying to guard against.

@jcouv
Copy link
Member

jcouv commented May 21, 2018

@jaredpar I think you answered a different question, and instead supported @Neme12's concern about ambiguities :-) #Closed

@Neme12
Copy link
Contributor

Neme12 commented May 21, 2018

@jaredpar Thanks for the response, although I'm a little confused. I was arguing against new contributing to type inference. But I didn't even think of this problem with compatibility. Thanks for the explanation.

I probably didn't make myself clear enough, which is my fault. By "why" I was asking why the proposed behavior is pity and was surprised that changing new to affect overload resolution would be considered a welcome change for a future version.

@jaredpar
Copy link
Member

Thanks for the response, although I'm a little confused. I was arguing against new contributing to type inference

Sorry. I was in a rush to finish my comment before another meeting started and misread your feedback.

But I didn't even think of this problem with compatibility.

My mind is twisted to think about evil things after years of compatibility bugs 😄

@CyrusNajmabadi
Copy link
Member

Why open a whole new can of worms?

I explicitly did not say that. I said precisely that: "that's likely a good place to start from"

:)

I simply said if there was any limitations here they could be considered in the future, not that htey should or that there was any problem here. It was a show of support for this approach precisely because it's likely good enough and because it likely doesn't shut down potential improvements in the future. Both of these are important for me when designing language features. I very much want to ensure that when decisions are made now, they don't shut down possible areas of interest that may arise later.

@CyrusNajmabadi
Copy link
Member

I thought this would work just like default - as a shorthand when it's absolutely clear what the target type is. If there's any sort of ambiguity, just specify the type you need. Or if you prefer, use named argument. I don't see any problem with that.

That seems totally fine to me. Importantly, i think that's a great place to start at. And, if it ends up potentially being limiting, is something that could potentially addressed in the future in a way that i do not think would cause breaking changes.

@CyrusNajmabadi
Copy link
Member

Now suddenly the consume is broken because new (1) is ambiguous between C1 and C2. Expanding this out it essentially means that evyr new constructur is potentially a source breaking change in the face of target typed new. This is the core scenario we're trying to guard against.

Well, this is a situation with "default" as well. If i have a method:

void M(int i) { }

And someone calls: M(default);

it's now source-breaking if i add:

void M(bool b) { }

because the 'default' is ambiguous.

I think that's an acceptable place to be in.

@jaredpar
Copy link
Member

@CyrusNajmabadi

Well, this is a situation with "default" as well.

The situation is a bit different with default. In the case of default the source breaking change comes when an overload of an existing method is added. That is an understood place where source breaks can happen which default does make worse.

In the case of target typed new the break comes when a constructor is added to a type. This ends up breaking an existing method overload that the type author was quite possible completely unaware of.

The way I think about target typed new is that for the purpose of overload resolution defining a new constructor is like defining a new implicit conversion on the type.

@CyrusNajmabadi
Copy link
Member

In the case of default the source breaking change comes when an overload of an existing method is added. That is an understood place where source breaks can happen which default does make worse.

I guess i long internalized that adding methods could trivially lead to source-breaks. So having a way for adding constructors to break things doesn't really bother me :)

That said, i'm not pushing for this to be supported. As i said, i think this is a good place to start from. And only at some future point would this need to be revisited if there was particular value in expanding support here.

@jaredpar
Copy link
Member

@CyrusNajmabadi

I guess i long internalized that adding methods could trivially lead to source-breaks.

Agree. I think the difference here for me is the proximity of the change to the break. When adding an overload I'm both:

  • Aware I'm introducing a potential break. The other overload is generally in the same source file and same section as the new code I'm adding.
  • Able to look at the set of overloads in totality and evaluate the risk of a breaking source change.

Those aren't absolutes: extension methods, partial classes, etc ... can make it harder. But generally speaking true. The same is not true with target type new. The new constructor and the overload it affects don't have to be in the same source file or even the same library.

@alrz alrz force-pushed the features/target-typed-new branch from 54cc99c to 3dd3a26 Compare May 22, 2018 11:21
@jcouv jcouv self-assigned this May 22, 2018
@jcouv
Copy link
Member

jcouv commented May 22, 2018

In short, an argument that is a target-typed new behaves like an out var in that it doesn't contribute a type to overload resolution.

From discussion with @gafter, my summary is incorrect.

Here's a better summary: A target-typed new is never a better "conversion from expression" to one type versus another.

Neal is recommending to wait for LDM to decide on the initializer scenario before moving ahead with this PR.
Here's the example:
M(new () { x = 3 }) where two overloads exist (M(C) and M(D)). Suppose that D doesn't have a property/field named x, does expression new () { x = 3 } convert to D or not?

For the record, some scenarios Neal explained to me:

  • [Updated] If you have Base and Derived, both with empty constructors, then M(new ()) would pick Derived because it is more specific. be ambiguous.
  • M(new (i: 1, j: 2)) when we have two overloads (one with C and the other with D) and two constructors C(int i, int j) and D(int x, int y). Only the overload with C is applicable because expression new (i: 1, j: 2) does not convert to D. #Closed

<Node Name="UnboundObjectCreationExpression" Base="BoundExpression">
<!-- Type is not significant for this node type; always null -->
<Field Name="Type" Type="TypeSymbol" Override="true" Null="always"/>
<Field Name="AnalyzedArguments" Type="AnalyzedArguments"/>
Copy link
Member

@jcouv jcouv Jul 31, 2018

Choose a reason for hiding this comment

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

AnalyzedArguments [](start = 17, length = 17)

I don't think we should hold onto a pooled object in the bound tree. It's unclear who's responsible for freeing it... #Closed

Copy link
Member Author

@alrz alrz Aug 1, 2018

Choose a reason for hiding this comment

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

So you're suggesting we postpone binding arguments (potentially until we create the conversion)? In that case, we should be responsible for binding the argument list in error cases when there's no destination type to convert to e.g. new(a, b);. #Closed

Copy link
Member

Choose a reason for hiding this comment

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

Some alternatives for discussion:

  1. we could bind arguments twice (once in BindObjectCreationExpression as today, and a second time, in CreateImplicitNewConversion, which would discard diagnostics), but that is somewhat wasteful
  2. we could use a non-pooled instance of AnalyzedArguments, but that is a lot of plumbing/infrastructure change to do cleanly (we'd want to split AnalyzedArguments into a non-poolable base type and a poolable derived type, modify consumers to work with non-poolable one, etc).

I'd suggest we go with (1) for now, with a PROTOTYPE marker.


In reply to: 206795081 [](ancestors = 206795081)

Copy link
Member Author

Choose a reason for hiding this comment

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

As yet another alternative, I think we could bind the arg list in CreateImplicitNewConversion and another time in some other phase (likely DiagnosticsPass?) when we encounter a UnboundObjectCreationExpression. This way, we only bind it once, for the error cases or otherwise.

}
finally
{
this.Reset(ref resetPoint);
this.Release(ref resetPoint);
}

isPossibleArrayCreation =
Copy link
Member

@jcouv jcouv Jul 31, 2018

Choose a reason for hiding this comment

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

isPossibleArrayCreation [](start = 12, length = 23)

Just curious: What prompted you to move the initialization of isPossibleArrayCreation outside of the try/finally? #Closed

Copy link
Member Author

@alrz alrz Aug 1, 2018

Choose a reason for hiding this comment

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

we capture all the information required in try block, so I thought this would be more clear. #Closed

@jcouv
Copy link
Member

jcouv commented Jul 31, 2018

        comp.VerifyDiagnostics(

Nice. Thanks! #Closed


Refers to: src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenTupleTest.cs:939 in 141f778. [](commit_id = 141f778, deletion_comment = False)

@@ -9,6 +9,1404 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests
{
public partial class IOperationTests : SemanticModelTestBase
{
[CompilerTrait(CompilerFeature.IOperation)]
[Fact]
public void TargetTypedObjectCreationWithMemberInitializers()
Copy link
Member

@jcouv jcouv Jul 31, 2018

Choose a reason for hiding this comment

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

Thanks for adding those IOperation tests!
Tagging @333fred @mavasani in case they want to take a look at those tests too. #Closed

OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null)
IArgumentOperation (ArgumentKind.DefaultValue, Matching Parameter: o) (OperationKind.Argument, Type: null, IsImplicit) (Syntax: '2')
ILiteralOperation (OperationKind.Literal, Type: System.Object, Constant: null, IsImplicit) (Syntax: '2')
InConversion: CommonConversion (Exists: True, IsIdentity: True,
Copy link
Member

Choose a reason for hiding this comment

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

} [](start = 8, length = 1)

If you don't mind, could you add one IOperations test with arguments in new(...) as well? Thanks

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

Done with review pass (iteration 35).
Only blocking concern is holding onto pooled object (AnalyzedArguments) in the bound tree. Aside from that a couple minor questions/suggestions.
Thanks!

@alrz alrz force-pushed the features/target-typed-new branch from 6628fda to 85e0a01 Compare August 2, 2018 10:31
@alrz alrz force-pushed the features/target-typed-new branch from 85e0a01 to aaa3b35 Compare August 2, 2018 10:38
Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

LGTM (iteration 37).
Thanks for sorting the AnalyzedArguments situation out.

@gafter
Copy link
Member

gafter commented Aug 2, 2018

I'm reviewing this.

@@ -1292,7 +1292,14 @@
<!-- BinderOpt is added as a temporary solution for IOperation implementation and should probably be removed in the future -->
<Field Name="BinderOpt" Type="Binder" Null="allow"/>
</Node>


<Node Name="UnboundObjectCreationExpression" Base="BoundExpression">
Copy link
Member

@gafter gafter Aug 3, 2018

Choose a reason for hiding this comment

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

Since the object initializer is logically part of the (un)bound object creation expression, it should be captured here. Probably as an optional InitializerExpressionSyntax. The caller should not assume that the syntax of this bound node is of the proper form, as that would prevent us from using it as an intermediary in translations (of perhaps new, different syntax forms) later.

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.

Please review the follow-up PR at #29167 after this goes in. thanks.

@@ -128,6 +133,126 @@ internal partial class Binder
{ WasCompilerGenerated = wasCompilerGenerated };
}

private BoundExpression CreateImplicitNewConversion(SyntaxNode syntax, BoundExpression source, Conversion conversion, bool isCast, TypeSymbol destination, DiagnosticBag diagnostics)
{
var node = (ObjectCreationExpressionSyntax)source.Syntax;
Copy link
Member

Choose a reason for hiding this comment

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

ObjectCreationExpressionSyntax [](start = 24, length = 30)

As I mentioned in BoundNodes.xml, we should not require a relationship between an UnboundObjectCreationExpression and its syntax node. Please store the initializer (or anything else needed) in the UnboundObjectCreationExpression.

var unboundObjectCreation = (UnboundObjectCreationExpression)source;

// PROTOTYPE(target-typed-new): Reconstruct AnalyzedArguments to avoid binding twice
arguments.Arguments.AddRange(unboundObjectCreation.Arguments);
Copy link
Member

Choose a reason for hiding this comment

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

I think this comment can be deleted now?

@checked: false,
explicitCastInCode: isCast,
constantValueOpt: null, // A "target-typed new" would never produce a constant.
type: destination);
Copy link
Member

@gafter gafter Aug 3, 2018

Choose a reason for hiding this comment

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

This should be explicit in the spec, if it isn't already, as the following corresponding code is accepted today:

        const int N = new int();

I think there is a rule that a value type's default constructor can't be used; that is what causes this (commented fact) to be true. #ByDesign

@@ -609,6 +609,9 @@ private static bool IsEncompassingImplicitConversionKind(ConversionKind kind)
case ConversionKind.ImplicitTupleLiteral:
case ConversionKind.ImplicitTuple:
case ConversionKind.ImplicitThrow:

// Added for C# 8.
case ConversionKind.ImplicitNew:
return true;
Copy link
Member

Choose a reason for hiding this comment

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

Do we have a test that exercises this branch?

Copy link
Member Author

Choose a reason for hiding this comment

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

Looks like it's not reachable for ImplicitThrow as well. My understanding is that since both new and throw are convertible to any type, there's no conversion left for these to encompass. Is that correct?


case ConversionKind.ImplicitNew:
return rewrittenOperand;

Copy link
Member

Choose a reason for hiding this comment

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

Is this branch exercised by tests? I'm wondering if it is 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.

Now that we directly return the bound object creation, it's not reachable. will revert.

);
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Can you please add tests to show how the semantic model works? In normal (non-error) cases, e.g. for the whole expression and its argument expressions, but also inside the object initializer, and inside the object initializer when there is no target type (error scenario), for example

    M(new() { X = <bind>N()</bind> }); // M not found, but N is found. Also try when M is found but no X is found

How does GetTypeInfo work on the subexpression N()? Even if it doesn't work, it would be nice to have a test documenting the current (expected?) behavior.

Copy link
Member Author

Choose a reason for hiding this comment

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

If it's ok I'll address these in the follow-up PR.

@gafter
Copy link
Member

gafter commented Aug 3, 2018

Done with review pass (Iteration 37). Generally it is looking quite good, except as noted.


In reply to: 410101220 [](ancestors = 410101220)

@alrz
Copy link
Member Author

alrz commented Aug 8, 2018

The next PR based on these changes is opened at #29167

Copy link
Member

@gafter gafter left a comment

Choose a reason for hiding this comment

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

:shipit:

@gafter gafter merged commit 1e246ca into dotnet:features/target-typed-new Aug 8, 2018
@alrz alrz deleted the features/target-typed-new branch August 8, 2018 17:16
@gafter gafter added the New Language Feature - Target-Typed New `new (args)` gets the created type inferred from context label Dec 15, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers Community The pull request was submitted by a contributor who is not a Microsoft employee. New Language Feature - Target-Typed New `new (args)` gets the created type inferred from context
Development

Successfully merging this pull request may close these issues.

None yet

7 participants