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

Collection expressions: require Add method callable with a single argument for class and struct types that do not use [CollectionBuilder] #72654

Merged
merged 19 commits into from
Mar 28, 2024

Conversation

cston
Copy link
Member

@cston cston commented Mar 21, 2024

Spec changes: dotnet/csharplang#8022

Fixes #72098

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Mar 21, 2024
@cston cston force-pushed the any-add branch 2 times, most recently from e2ac0e8 to 051d5ec Compare March 22, 2024 00:31
…ent for class and struct types that do not use [CollectionBuilder]
// Any trailing parameters must be optional or params.
for (int i = requiredLength; i < parameters.Length; i++)
{
if (parameters[i] is { IsOptional: false, IsParams: false })
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 22, 2024

Choose a reason for hiding this comment

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

IsParams

The IsParams check is inconsistent with how overload resolution does that.

  • Only value on corresponding least overridden method matters
  • Only value on the last parameter matters.
  • The definition type of the least overridden method parameter matters, see OverloadResolution.IsValidParams. In fact I suggest to use that helper here #Closed

return true;
}

var implicitReceiver = new BoundObjectOrCollectionValuePlaceholder(syntax, isNewInstance: true, targetType) { WasCompilerGenerated = true };
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 22, 2024

Choose a reason for hiding this comment

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

var implicitReceiver = new BoundObjectOrCollectionValuePlaceholder(syntax, isNewInstance: true, targetType) { WasCompilerGenerated = true };

I assume the logic starting from here to the end of the method corresponds to what is happening in MethodGroupResolution Binder.BindExtensionMethod. Consider extracting this code into a local function, adding a comment referring to the "prototype" API (Binder.BindExtensionMethod), adding a comment to Binder.BindExtensionMethod stating that relevant changes there should be mirrored in the local function here. This will make it easier to make sure that necessary adjustments will be made to properly handle extension types. #Closed

var useSiteInfo = CompoundUseSiteInfo<AssemblySymbol>.Discarded;

var lookupResult = LookupResult.GetInstance();
LookupInstanceMember(lookupResult, targetType, leftIsBaseReference: false, rightName: methodName, rightArity: 0, invoked: true, ref useSiteInfo);
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 22, 2024

Choose a reason for hiding this comment

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

LookupInstanceMember

Rather than trying to duplicate the lookup code, I think we should use Binder.BindInstanceMemberAccess and proceed only if we get a BoundMethodGroup back. I am thinking of a call similar to the one in HasCollectionExpressionApplicableAddMethod.makeInvocationExpression. Afterwards, I think we should pay attention to the BoundMethodGroup.SearchExtensionMethods value before looking for extensions. #Closed

}
}

// Value parameter must be by value, in, or ref readonly.
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 22, 2024

Choose a reason for hiding this comment

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

// Value parameter must be by value, in, or ref readonly.

Are there ref restrictions for optional parameters? #Closed

return false;
}

// If the method is generic, can the type arguments be inferred from the one or two arguments?
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 22, 2024

Choose a reason for hiding this comment

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

can the type arguments be inferred

I assume we can talk only about a possibility of inference. The check below doesn't really guarantee that the inference is going to succeed. #Closed

var typeParameters = method.TypeParameters;
if (typeParameters.Length > 0)
{
var usedParameterTypes = parameters.Slice(0, requiredLength).SelectAsArray(p => p.Type);
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 22, 2024

Choose a reason for hiding this comment

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

SelectAsArray

What is the result type of Slice? Do we really need to allocate an array? Could we perhaps use an ArrayBuilder from a pool? #Closed

Copy link
Member Author

@cston cston Mar 25, 2024

Choose a reason for hiding this comment

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

Changed to AsSpan(0, requiredLength).

var typeParameters = method.TypeParameters;
if (typeParameters.Length > 0)
{
var usedParameterTypes = parameters.Slice(0, requiredLength).SelectAsArray(p => p.Type);
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 22, 2024

Choose a reason for hiding this comment

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

p => p.Type

The projection doesn't look necessary, we should be able to get to the type from ParameterSymbol right when it is needed #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 22, 2024

        // For purpose of conversion, we rely the existence of an Add method.

Typo? #Closed


Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:26813 in 442498d. [](commit_id = 442498d, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 22, 2024

        // For purpose of conversion, we rely the existence of an Add method.

Typo? #Closed


Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:26841 in 442498d. [](commit_id = 442498d, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 22, 2024

                private void Add(int i) { _list.Add(i is T t ? t : default); }

Do we have a test demonstrating that inaccessible instance method doesn't prevent us from finding an extension method? #Closed


Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:34333 in 442498d. [](commit_id = 442498d, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 22, 2024

                public static void Add<T>(this MyCollection<T> collection, {{refKind}} T t) { collection.__AddInternal(t); }

Are there ref restrictions for this parameter of extension method? #Closed


Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:34608 in 442498d. [](commit_id = 442498d, deletion_comment = False)

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 22, 2024

                public void Add(A a) => throw null;

It is probably worth adding a similar test but with type for an additional params collection parameter missing instead. It would be good to observe what effect this is going to have on conversion classification. #Closed


Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:36190 in 442498d. [](commit_id = 442498d, deletion_comment = False)


return false;

static bool isApplicableAddMethod(MethodSymbol method, bool expectingExtensionMethod)
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 22, 2024

Choose a reason for hiding this comment

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

isApplicableAddMethod

We probably should pay attention to use-site diagnostics (ignore methods with errors), and make consumers of HasCollectionExpressionAddMethod aware of them in case of a failure. #Closed

@AlekseyTs
Copy link
Contributor

AlekseyTs commented Mar 22, 2024

It looks like there are legitimate test failures in the IOperation leg. #Resolved

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 1)

@@ -1285,6 +1285,85 @@ internal bool HasCollectionExpressionApplicableAddMethod(SyntaxNode syntax, Type
}
}

internal bool HasCollectionExpressionAddMethod(SyntaxNode syntax, TypeSymbol targetType)
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 22, 2024

Choose a reason for hiding this comment

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

HasCollectionExpressionAddMethod

I am not sure how easy it will be to come up with a scenario making importance of this obvious, but I think this helper should keep track of all dependencies (ref assemblies) that were involved on the path to success and make the caller aware of that set. I do not think the set must be explicitly narrowed, i.e. no reason to trim dependencies from "failed" attempts. Our primary concern is to not drop dependencies for "success". #Closed

switch (element)
{
case BoundCall call:
return call.Arguments[call.InvokedAsExtensionMethod ? 1 : 0];
Copy link
Contributor

@AlekseyTs AlekseyTs Mar 25, 2024

Choose a reason for hiding this comment

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

return call.Arguments[call.InvokedAsExtensionMethod ? 1 : 0];

This logic looks suspicious. It is not obvious what are we trying to accomplish and why. It looks like BoundCollectionExpression holds onto the original UnconvertedCollectionExpression node. Could we. perhaps, use information from that node instead? Not sure what do we want to do with expressions that do not have a natural type though. #Closed

Copy link
Contributor

Choose a reason for hiding this comment

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

It feels like it would be better to place this special logic into CreateBoundCollectionExpressionSpreadElement, where it would make sense as an error recovery logic. It doesn't feel like it belongs in this general helper because we don't really know where the element node is coming from and whether it is appropriate to dig into it this way.

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 case is to unwrap the Add() argument when bindCollectionInitializerElementAddMethod fails in overload resolution with one or more inapplicable or ambiguous Add methods. We hit this case for spreads and for regular elements.

Copy link
Contributor

@AlekseyTs AlekseyTs Mar 26, 2024

Choose a reason for hiding this comment

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

We hit this case for spreads and for regular elements.

Thanks for the clarification. Consider adding a comment what the code is trying to accomplish and why it makes sense to do what it is doing.

@AlekseyTs
Copy link
Contributor

Done with review pass (commit 2)

@cston
Copy link
Member Author

cston commented Mar 25, 2024

                private void Add(int i) { _list.Add(i is T t ? t : default); }

Added AddMethod_Accessibility_03.


In reply to: 2015424773


Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:34333 in 442498d. [](commit_id = 442498d, deletion_comment = False)

@cston cston requested review from a team as code owners March 27, 2024 14:20
Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 13), assuming CI is passing

@cston
Copy link
Member Author

cston commented Mar 27, 2024

@dotnet/roslyn-compiler for a second review, thanks.

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 17)

Copy link
Member

@333fred 333fred left a comment

Choose a reason for hiding this comment

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

Generally this looks good, and I don't think any of my test suggestions should block merging, so I'm signing off. But let's get those tests in in a separate PR.

@@ -4521,12 +4509,18 @@ static void Main()
// (7,13): error CS1061: 'string' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)
// s = [default];
Diagnostic(ErrorCode.ERR_NoSuchMemberOrExtension, "[default]").WithArguments("string", "Add").WithLocation(7, 13),
// (7,13): error CS9215: Collection expression type 'string' must have an instance or extension method 'Add' that can be called with a single argument.
Copy link
Member

Choose a reason for hiding this comment

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

Can we do something to suppress one of the duplicate errors here?

[Theory]
[InlineData("")]
[InlineData("in")]
[InlineData("ref readonly")]
Copy link
Member

@333fred 333fred Mar 28, 2024

Choose a reason for hiding this comment

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

Why does this work for ref readonly? I would, at the very least, expect a warning? #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

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

Why does this work for ref readonly?

Why do you think it shouldn't?

I would, at the very least, expect a warning?

A warning where? Saying what?

Are you referring to: "warning CS9192: Argument 1 should be passed with 'ref' or 'in' keyword"

Note, we intentionally do not report it for MyCollection<C> z = new () {x}; today because there is no place to add any of the suggested keywords to make the warning to go away.

[Theory]
[InlineData("")]
[InlineData("in")]
[InlineData("ref readonly")]
Copy link
Member

@333fred 333fred Mar 28, 2024

Choose a reason for hiding this comment

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

Why does this work for ref readonly? I would, at the very least, expect a warning? #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

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

Same response, I guess.

[Theory]
[InlineData("")]
[InlineData("in")]
[InlineData("ref readonly")]
Copy link
Member

@333fred 333fred Mar 28, 2024

Choose a reason for hiding this comment

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

Why does this work for ref readonly? I would, at the very least, expect a warning? #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

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

Same response, I guess.

[Theory]
[InlineData("")]
[InlineData("in")]
[InlineData("ref readonly")]
Copy link
Member

@333fred 333fred Mar 28, 2024

Choose a reason for hiding this comment

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

Why does this work for ref readonly? I would, at the very least, expect a warning? #Resolved

Copy link
Contributor

Choose a reason for hiding this comment

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

Same response, I guess.

.method public instance void Add(!T x, !T y)
{
.param [2]
.custom instance void [mscorlib]System.ParamArrayAttribute::.ctor() = ( 01 00 00 00 )
Copy link
Member

@333fred 333fred Mar 28, 2024

Choose a reason for hiding this comment

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

Should we have a version with ParamCollectionAttribute? #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.

Should we have a version with ParamCollectionAttribute?

AddMethod_ParamCollectionAttribute_03

int x = 1;
int[] y = [2, 3];
MyCollection<int> z = [x, ..y];
MyCollection<int> w = new() { x };
Copy link
Member

Choose a reason for hiding this comment

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

What if we do MyCollection<int[]>?

}

[Fact]
public void TargetTypedElement_PublicAPI_List()
public void AddMethod_Extension_12_WrongThisType()
Copy link
Member

@333fred 333fred Mar 28, 2024

Choose a reason for hiding this comment

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

Let's have a version of this one with a variant interface: IE, the Add method is defined as an extension on IEnumerable<object>, when I have a MyCollection<string> and it implements IEnumerable<string>. #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.

Added AddMethod_Extension_13_WrongThisType.

comp.VerifyEmitDiagnostics(
// (11,36): error CS1103: The first parameter of an extension method cannot be of type 'dynamic'
// public static void Add<T>(this dynamic d, T t) { d.__AddInternal(t); }
Diagnostic(ErrorCode.ERR_BadTypeforThis, "dynamic").WithArguments("dynamic").WithLocation(11, 36));
Copy link
Member

Choose a reason for hiding this comment

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

And what happens when we see this in IL? Will we call it from Main in source?

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private List<MyCollection?> GetList() => _list ??= new();
unsafe public void Add(void* p) => throw null;
Copy link
Member

@333fred 333fred Mar 28, 2024

Choose a reason for hiding this comment

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

Let's have a variant where this is the closest extension method, but there's a better (safe) extension method further away. #Resolved

{
return Conversion.NoConversion;
}

// PROTOTYPE: Probably should at least accumulate dependencies and propagate them in case of success.
}

Copy link
Member Author

Choose a reason for hiding this comment

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

// PROTOTYPE: Probably should at least accumulate dependencies and propagate them in case of success.

Logged #72770.

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 18)

@cston cston enabled auto-merge (squash) March 28, 2024 05:32
@cston cston disabled auto-merge March 28, 2024 05:32
@cston cston enabled auto-merge (squash) March 28, 2024 05:33
@jjonescz
Copy link
Member

jjonescz commented Mar 28, 2024

Looks like there's legitimate test failure in UsedAssemblies leg for CollectionExpressionTests.AddMethod_RefOmittedArguments (assertion failed at Microsoft.CodeAnalysis.CSharp.LocalRewriter.MakeCollectionInitializer(BoundExpression rewrittenReceiver, BoundCollectionElementInitializer initializer) in /_/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ObjectOrCollectionInitializerExpression.cs:line 145)

@cston cston disabled auto-merge March 28, 2024 10:23
@cston cston enabled auto-merge (squash) March 28, 2024 10:25
@cston cston merged commit 711ee10 into dotnet:features/CollectionAdd Mar 28, 2024
25 of 30 checks passed
@cston cston deleted the any-add branch March 28, 2024 11:48
@AlekseyTs
Copy link
Contributor

Looks like there's legitimate test failure in UsedAssemblies leg for CollectionExpressionTests.AddMethod_RefOmittedArguments (assertion failed at Microsoft.CodeAnalysis.CSharp.LocalRewriter.MakeCollectionInitializer(BoundExpression rewrittenReceiver, BoundCollectionElementInitializer initializer) in /_/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ObjectOrCollectionInitializerExpression.cs:line 145)

How the CI passed then?

@jjonescz
Copy link
Member

jjonescz commented Mar 28, 2024

How the CI passed then?

The test was marked as skipped in the last commit (1316830)

@AlekseyTs
Copy link
Contributor

The assert is not specific to the UsedAssemblies leg, I think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

False positive for CS1503 with MSBuild 17.10, but not dotnet build
4 participants