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
Support target-typed new #25196
Conversation
{ | ||
// 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 |
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 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.
Thanks. Marked as blocked until LDM reviews the language design proposal. #Closed |
4c1b011
to
54cc99c
Compare
Marking as "for personal review" for now, so it doesn't show in our "active reviews" query. |
@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). 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. |
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 |
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 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.
Can you give an example of code where this would be worth pitying over? |
I'm sorry, that's just how I see it. Please give me an example of where I thought this would work just like |
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
Now suddenly the consume is broken because |
@jaredpar Thanks for the response, although I'm a little confused. I was arguing against 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 |
Sorry. I was in a rush to finish my comment before another meeting started and misread your feedback.
My mind is twisted to think about evil things after years of compatibility bugs 😄 |
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. |
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. |
Well, this is a situation with "default" as well. If i have a method: void M(int i) { } And someone calls: 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. |
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. |
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. |
Agree. I think the difference here for me is the proximity of the change to the break. When adding an overload I'm both:
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. |
54cc99c
to
3dd3a26
Compare
From discussion with @gafter, my summary is incorrect. Here's a better summary: A Neal is recommending to wait for LDM to decide on the initializer scenario before moving ahead with this PR. For the record, some scenarios Neal explained to me:
|
<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"/> |
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.
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
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.
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
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.
Some alternatives for discussion:
- we could bind arguments twice (once in
BindObjectCreationExpression
as today, and a second time, inCreateImplicitNewConversion
, which would discard diagnostics), but that is somewhat wasteful - 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)
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.
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 = |
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.
isPossibleArrayCreation [](start = 12, length = 23)
Just curious: What prompted you to move the initialization of isPossibleArrayCreation
outside of the try/finally? #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.
we capture all the information required in try
block, so I thought this would be more clear. #Closed
@@ -9,6 +9,1404 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests | |||
{ | |||
public partial class IOperationTests : SemanticModelTestBase | |||
{ | |||
[CompilerTrait(CompilerFeature.IOperation)] | |||
[Fact] | |||
public void TargetTypedObjectCreationWithMemberInitializers() |
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.
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, |
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.
} [](start = 8, length = 1)
If you don't mind, could you add one IOperations test with arguments in new(...)
as well? Thanks
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 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!
6628fda
to
85e0a01
Compare
85e0a01
to
aaa3b35
Compare
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 (iteration 37).
Thanks for sorting the AnalyzedArguments situation out.
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"> |
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 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.
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.
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; |
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.
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); |
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 comment can be deleted now?
@checked: false, | ||
explicitCastInCode: isCast, | ||
constantValueOpt: null, // A "target-typed new" would never produce a constant. | ||
type: destination); |
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 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; |
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.
Do we have a test that exercises this branch?
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.
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; | ||
|
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 this branch exercised by tests? I'm wondering if it is 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.
Now that we directly return the bound object creation, it's not reachable. will revert.
); | ||
} | ||
} | ||
} |
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.
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.
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 it's ok I'll address these in the follow-up PR.
Done with review pass (Iteration 37). Generally it is looking quite good, except as noted. In reply to: 410101220 [](ancestors = 410101220) |
The next PR based on these changes is opened at #29167 |
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.
Proposal: https://github.com/dotnet/csharplang/blob/master/proposals/target-typed-new.md
Test plan: #28489