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 literals natural type #67364

Closed
wants to merge 10 commits into from

Conversation

cston
Copy link
Member

@cston cston commented Mar 18, 2023

Proposal: collection-literals.md
Test plan: #66418

var useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics);
// PROTOTYPE: Confirm with LDM that we use the element expressions rather than
// the element types to infer best common type.
var commonType = BestTypeInferrer.InferBestType(node.Initializers, Conversions, ref useSiteInfo, out _);
Copy link
Member

Choose a reason for hiding this comment

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

yes, using hte expressions is my expectation here. i would also expect this to match what we do for new[] { x, y, z }

collectionType = GetWellKnownType(WellKnownType.System_Collections_Generic_List_T, diagnostics, location).Construct(commonType);
}

// PROTOTYPE: hasErrors is dropped.
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Mar 18, 2023

Choose a reason for hiding this comment

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

maybe prototype comment about binding to the int capacity constructor. #Resolved

Copy link
Member

Choose a reason for hiding this comment

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

we'll probably want LDM sign off on that behavior as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

maybe prototype comment about binding to the int capacity constructor.

Added.

@cston cston force-pushed the collections-2 branch 3 times, most recently from eb7fcc3 to bb8bc82 Compare March 21, 2023 06:28
@cston cston force-pushed the collections-2 branch 2 times, most recently from 90e23a9 to 63504c2 Compare April 6, 2023 21:25
@cston cston force-pushed the collections-2 branch 2 times, most recently from 52a561d to eafbd88 Compare April 16, 2023 15:39
@cston cston marked this pull request as ready for review April 17, 2023 22:25
@cston cston requested a review from a team as a code owner April 17, 2023 22:25
@RikkiGibson RikkiGibson self-assigned this Apr 27, 2023

// This implementation differs from Binder.CollectionInitializerTypeImplementsIEnumerable().
// That method checks for an implicit conversion from IEnumerable to the collection type,
// but that would allow: Nullable<StructCollection> s = [];
Copy link
Contributor

@RikkiGibson RikkiGibson Apr 27, 2023

Choose a reason for hiding this comment

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

Should we adjust the behavior of that method and use it here? It seems like new StructCollection? { 1, 2, 3 } is always broken. Currently it seems like this errors because no Add method is available. But it definitely wouldn't make sense for this collection initializer to work, using the IEnumerable-ness of the underlying type, and the Add method of the nullable type. #Resolved

Copy link
Member

Choose a reason for hiding this comment

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

Agreed. I don't like these being different.

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 a PROTOTYPE comment.

: targetType.AllInterfacesNoUseSiteDiagnostics;
return allInterfaces.Any(static (a, b) => a.Equals(b, TypeCompareKind.AllIgnoreOptions), ienumerableType);
}

BoundExpression convertArrayElement(BoundExpression element, TypeSymbol elementType, BindingDiagnosticBag diagnostics)
Copy link
Contributor

Choose a reason for hiding this comment

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

Are there any functions here which have just been moved but not changed?

// Check that each element (even those without a type) can be converted to the best type.
foreach (var initializer in initializers)
{
if (!Conversions.ClassifyImplicitConversionFromExpression(initializer, bestType, ref useSiteInfo).Exists)
Copy link
Contributor

Choose a reason for hiding this comment

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

So it's possible to choose a best type for a set of expressions, even if one of the expressions is not convertible to that type? That seems so strange.

Copy link
Member

Choose a reason for hiding this comment

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

Why would we clear this? It seems like the better experience would be to let it be found as the best type and then have conversion errors?

Copy link
Member Author

Choose a reason for hiding this comment

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

So it's possible to choose a best type for a set of expressions, even if one of the expressions is not convertible to that type?

BestTypeInferrer.InferBestType() ignores expressions that do not contribute a type, so we'll choose a type of int for [1, null] for instance.

Why would we clear this? It seems like the better experience would be to let it be found as the best type and then have conversion errors?

Perhaps, although for reference, this matches the behavior for switch expressions, see SwitchExpressionBinder.InferResultType().

@@ -5571,7 +5638,7 @@ private BoundExpression BindUnexpectedComplexElementInitializer(InitializerExpre
}

private BoundExpression BindCollectionInitializerElementAddMethod(
ExpressionSyntax elementInitializer,
SyntaxNode elementInitializer,
Copy link
Contributor

@RikkiGibson RikkiGibson Apr 28, 2023

Choose a reason for hiding this comment

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

When is this not an expression syntax? Or was this just changed for convenience (removing need for casting?) #Resolved

Copy link
Member Author

@cston cston May 6, 2023

Choose a reason for hiding this comment

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

Currently, we can end up here for spread elements and dictionary elements when the element is a BoundBadExpression, but it seems questionable to call this method in those cases. I've added a PROTOTYPE comment to revisit in an upcoming PR.


case BoundKind.UnconvertedCollectionLiteralExpression:
{
if (operand.Type is null)
Copy link
Contributor

@RikkiGibson RikkiGibson Apr 28, 2023

Choose a reason for hiding this comment

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

What is generally signaled by operand.Type being null? Lack of a best type among the collection elements? #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.

Yes.

@@ -1309,7 +1309,6 @@ private IOperation CreateBoundCollectionInitializerCollectionLiteralExpression(B
}
else
{
// PROTOTYPE: Temporary until natural type is supported.
Copy link
Contributor

@RikkiGibson RikkiGibson Apr 28, 2023

Choose a reason for hiding this comment

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

I'm assuming we now enter the previous 'if' in the natural type scenario? #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.

Yes, that's correct.

Copy link
Contributor

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

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

Done review pass. Had some clarifying questions and suggestions. Haven't reviewed tests yet.

@@ -657,7 +658,8 @@ private Conversion ClassifyStandardImplicitConversion(BoundExpression sourceExpr

Conversion conversion = ClassifyImplicitBuiltInConversionFromExpression(sourceExpression, source, destination, ref useSiteInfo);
if (conversion.Exists &&
!conversion.IsInterpolatedStringHandler)
!conversion.IsInterpolatedStringHandler &&
!conversion.IsCollectionLiteral)
Copy link
Member

@333fred 333fred Apr 28, 2023

Choose a reason for hiding this comment

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

Comment above needs to be updated as well, last paragraph calling out exceptions. #Resolved


if (destination is ArrayTypeSymbol arrayType)
{
if (arrayType.IsSZArray)
Copy link
Member

@333fred 333fred Apr 28, 2023

Choose a reason for hiding this comment

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

Are there intentions to support MDArrays, with a syntax like [[1, 2], [3, 4]]? #Pending

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 to open questions in the test plan #66418.

static bool isSpanType(CSharpCompilation compilation, TypeSymbol targetType, WellKnownType spanType, [NotNullWhen(true)] out TypeSymbol? elementType)
{
if (targetType is NamedTypeSymbol { Arity: 1 } namedType
&& TypeSymbol.Equals(namedType.OriginalDefinition, compilation.GetWellKnownType(spanType), TypeCompareKind.AllIgnoreOptions))
Copy link
Member

@333fred 333fred Apr 28, 2023

Choose a reason for hiding this comment

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

Don't we need to call the binder version of GetWellKnownType that tracks dependencies? #Resolved

Copy link
Member Author

@cston cston May 6, 2023

Choose a reason for hiding this comment

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

This is checking if the given type from the compilation is a span type. If it's not, that shouldn't affect dependencies, and if it is, the compilation should already be tracking the dependency.

return false;
}

static bool supportsCollectionInitializer(CSharpCompilation compilation, TypeSymbol targetType)
Copy link
Member

@333fred 333fred Apr 28, 2023

Choose a reason for hiding this comment

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

This is missing the Add checks on whether a type supports collection initializers. Perhaps rename this to just typeImplementsIEnumerable? #Resolved


// This implementation differs from Binder.CollectionInitializerTypeImplementsIEnumerable().
// That method checks for an implicit conversion from IEnumerable to the collection type,
// but that would allow: Nullable<StructCollection> s = [];
Copy link
Member

Choose a reason for hiding this comment

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

Agreed. I don't like these being different.

// Check that each element (even those without a type) can be converted to the best type.
foreach (var initializer in initializers)
{
if (!Conversions.ClassifyImplicitConversionFromExpression(initializer, bestType, ref useSiteInfo).Exists)
Copy link
Member

Choose a reason for hiding this comment

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

Why would we clear this? It seems like the better experience would be to let it be found as the best type and then have conversion errors?


if (bestType is { })
{
return Compilation.GetWellKnownType(WellKnownType.System_Collections_Generic_List_T).Construct(bestType);
Copy link
Member

@333fred 333fred Apr 28, 2023

Choose a reason for hiding this comment

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

Please test when List<T> isn't found. #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.

CollectionLiteralTests.MissingList.

Debug.Assert(false); // PROTOTYPE: Add test.
// There was an explicit cast on top of this
type = convertedCollection.NaturalTypeOpt;
Debug.Assert(false); // Add test if this assert fails.
Copy link
Member

@333fred 333fred Apr 28, 2023

Choose a reason for hiding this comment

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

throw unreachable? #Resolved

Copy link
Member Author

@cston cston May 6, 2023

Choose a reason for hiding this comment

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

I think I'd rather return the values below, to avoid an unhandled exception. (The returned values follow other cases here.)

var model = comp.GetSemanticModel(tree);
var collections = tree.GetRoot().DescendantNodes().OfType<CollectionCreationExpressionSyntax>().ToArray();
Assert.Equal(3, collections.Length);
VerifyTypes(model, collections[0], "?", "System.Object", ConversionKind.NoConversion);
Copy link
Member

@333fred 333fred Apr 28, 2023

Choose a reason for hiding this comment

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

Please use named args with the constants. Sorry. #Resolved

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.

Done review pass (commit 8)

// (6,18): error CS0103: The name 'Unknown' does not exist in the current context
// var b = [Unknown, 2];
Diagnostic(ErrorCode.ERR_NameNotInContext, "Unknown").WithArguments("Unknown").WithLocation(6, 18),
// (7,17): error CS9503: No best type found for implicitly-typed collection literal.
Copy link
Member

Choose a reason for hiding this comment

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

It feels likely that we will have a better error experience if we ignore error types when finding the BCT of a collection literal. Less cascading errors down through the rest of the file, for example.

string source = """
class Program
{
static object F1((int, int) a, (int X, int Y) b)
Copy link
Member

@333fred 333fred Apr 28, 2023

Choose a reason for hiding this comment

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

Is there a test with a having different names, not just no names? #Resolved

var c1 = [a, b];
return c1;
}
static object F2(dynamic[] a, object[] b)
Copy link
Member

@333fred 333fred Apr 28, 2023

Choose a reason for hiding this comment

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

Worth testing spreads for this and below? #Pending

Copy link
Member Author

Choose a reason for hiding this comment

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

Testing spreads containing with element types object and dynamic will be included in #67942.

}
}
""";
var comp = CreateCompilation(new[] { source, s_collectionExtensions }, options: TestOptions.ReleaseExe, targetFramework: TargetFramework.Net70);
Copy link
Member

@333fred 333fred Apr 28, 2023

Choose a reason for hiding this comment

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

Consider verifying the .NET 6 behavior as well. #Resolved

}
static void F(bool b)
{
var x = b ? [1] : [];
Copy link
Member

@333fred 333fred Apr 28, 2023

Choose a reason for hiding this comment

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

Consider a version of this with [null] in the other hand. Also consider a version where they're target-typed to int?[] #Resolved

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see you have _32 for the null suggestion. Consider adding one for the target-typed suggestion 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.

Added TargetType_01 and _02.

}
}
""";
// PROTOTYPE: Should these calls be ambiguous when we treat IEnumerable<T> as a valid target type (since it is an interface of List<T>)?
Copy link
Member

@333fred 333fred Apr 28, 2023

Choose a reason for hiding this comment

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

int[] implements IEnumerable<int>, so I expect that no, these would not be ambiguous. #Resolved

Copy link
Member Author

@cston cston May 6, 2023

Choose a reason for hiding this comment

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

If int[] and IEnumerable<T> are both considered constructible collection types, should better function member prefer one over the other?

I've updated the PROTOTYPE comment and added an open question to the test plan #66418.

Copy link
Member

Choose a reason for hiding this comment

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

i def think we would want a preference. having an array and an IEnumerable overload would be pretty normal. Having that be ambiguous woudl be unfortunate.

}
}
""";
// PROTOTYPE: Should these calls be ambiguous when we treat IEnumerable<T> as a valid target type (since it is an interface of List<T>)?
Copy link
Member

@333fred 333fred Apr 28, 2023

Choose a reason for hiding this comment

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

Same comment. #Resolved

using System.Collections.Generic;
class Program
{
static List<int> F1(List<int> arg) => arg;
Copy link
Member

@333fred 333fred Apr 28, 2023

Choose a reason for hiding this comment

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

Consider a version of this test with int? and long type args. #Resolved

{
string source = """
string sourceA = """
namespace System
Copy link
Member

@333fred 333fred Apr 28, 2023

Choose a reason for hiding this comment

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

Can we not just use MakeTypeMissing/MakeMemberMissing (for the test below)? #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.

We could, and I have used MakeTypeMissing() and MakeMemberMissing() for other tests. But testing without the type at all seems useful.

@cston
Copy link
Member Author

cston commented May 6, 2023

Marking as draft while natural type is being discussed.

@cston cston marked this pull request as draft May 6, 2023 18:42
@cston cston closed this Aug 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants