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

Nullable conversion of collection expressions #69852

Merged
merged 19 commits into from
Sep 14, 2023

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Sep 7, 2023

Closes #69447
Relates to test plan #66418

@jcouv jcouv self-assigned this Sep 7, 2023
@dotnet-issue-labeler dotnet-issue-labeler bot added Area-Compilers untriaged Issues and PRs which have not yet been triaged by a lead labels Sep 7, 2023
@jcouv jcouv force-pushed the nullable-collections branch 2 times, most recently from 262af09 to e92a30f Compare September 7, 2023 21:47
@jcouv jcouv added this to the 17.8 milestone Sep 7, 2023
@jcouv jcouv marked this pull request as ready for review September 7, 2023 23:42
@jcouv jcouv requested a review from a team as a code owner September 7, 2023 23:42
{
static MyCollection<int>? M()
{
return (MyCollection<int>?)[1, 2, 3];
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Sep 8, 2023

Choose a reason for hiding this comment

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

can you have a test with (MyCollection<byte>?)[1, 2, 3]; #Resolved

@@ -541,6 +543,14 @@ void checkConstraintLanguageVersionAndRuntimeSupportForConversion(SyntaxNode syn
Conversion conversion,
BindingDiagnosticBag diagnostics)
{
if (targetType.IsNullableType())
{
Debug.Assert(conversion.IsNullable);
Copy link
Member

@cston cston Sep 8, 2023

Choose a reason for hiding this comment

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

Debug.Assert(conversion.IsNullable);

Is this true for int? x = [1, 2, 3];? #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.

Good catch. Thanks

private static bool IsSystemNullable(TypeSymbol type, [NotNullWhen(true)] out TypeSymbol? underlyingType)
{
if (type is NamedTypeSymbol nt
&& nt.OriginalDefinition.GetSpecialTypeSafe() == SpecialType.System_Nullable_T)
Copy link
Member

@cston cston Sep 8, 2023

Choose a reason for hiding this comment

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

It looks like we can use the SpecialType property directly. Consider combining all and using a pattern:

type is NamedTypeSymbol
{
    OriginalDefinition.SpecialType: SpecialType.System_Nullable_T,
    TypeArgumentsWithAnnotationsNoUseSiteDiagnostics: [var typeArg]
}
``` #Resolved

Copy link
Member

Choose a reason for hiding this comment

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

Actually, given that this method is used for several existing callers that were using GetSpecialTypeSafe(), perhaps it makes sense to keep as GetSpecialTypeSafe() to reduce risk.

@@ -4705,10 +4790,27 @@ class Program
}
Copy link
Member

@cston cston Sep 8, 2023

Choose a reason for hiding this comment

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

}

Consider updating the test to execute these methods, and perhaps additional methods with values, similar to TypeParameter_01. It will require adding a List<T> _list; field to S<T>.

static T Create3<T, U>(U x, U y) where T : struct, I<U> => [x, y];
static T? Create4<T, U>(U x, U y) where T : struct, I<U> => [x, y];
``` #Resolved


var verifier = CompileAndVerify(comp, expectedOutput: IncludeExpectedOutput("[1, 2, 3],"), verify: Verification.Fails);

// ILVerify:
Copy link
Member Author

@jcouv jcouv Sep 8, 2023

Choose a reason for hiding this comment

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

@cston I found this surprising. Let's discuss offline #Resolved

Copy link
Member

Choose a reason for hiding this comment

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

Consider removing or slimming down this comment.

Copy link
Member Author

Choose a reason for hiding this comment

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

Filed #69860 to track refreshing ILVerify bits and filing follow-up runtime repo issue if needed.

@jcouv
Copy link
Member Author

jcouv commented Sep 8, 2023

@333fred @RikkiGibson for another review. Thanks

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

var underlyingConversion = GetCollectionExpressionConversion(collectionExpression, underlyingDestination, ref useSiteInfo);
if (underlyingConversion.Exists)
{
return new Conversion(ConversionKind.ImplicitNullable, ImmutableArray.Create(underlyingConversion));
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 we need to update the speclet to more correctly convey the logic here. It currently states that a collection expression conversion exists from a collection expression to T?, where there exists a collection expression conversion from a collection expression to the underlying type T if T? is a nullable value type. We instead want the speclet to convey that some types of implicit conversions are allowed on top of collection expression conversions, such as implicit nullable conversions.

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 leave that up to Chuck who made the spec update

@@ -233,7 +233,9 @@ static bool filterConversion(Conversion conversion)

if (source.Kind == BoundKind.UnconvertedCollectionExpression)
{
Debug.Assert(conversion.IsCollectionExpression || !conversion.Exists);
Debug.Assert(conversion.IsCollectionExpression
|| (conversion.IsNullable && conversion.UnderlyingConversions[0].IsCollectionExpression)
Copy link
Member

@333fred 333fred Sep 8, 2023

Choose a reason for hiding this comment

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

Nit:

Suggested change
|| (conversion.IsNullable && conversion.UnderlyingConversions[0].IsCollectionExpression)
|| (conversion is { IsNullable: true, UnderlyingConversions: [{ IsCollectionExpression: true }] })
``` #ByDesign

}
else if (boundNodeForSyntacticParent is BoundConversion parentConversion)
{
// There was an explicit cast on top of the collection expression.
Copy link
Member

@333fred 333fred Sep 8, 2023

Choose a reason for hiding this comment

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

In this case, I would not expect ConvertedType to contain something different than the current type. ConvertedType is explicitly documented as implicit conversions only. #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 didn't know that. Thanks for pointing it out!


var typeInfo = model.GetTypeInfo(value);
Assert.Null(typeInfo.Type);
Assert.Equal("MyCollection<System.Int32>", typeInfo.ConvertedType.ToTestDisplayString());
Copy link
Member Author

@jcouv jcouv Sep 8, 2023

Choose a reason for hiding this comment

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

Tagging @AlekseyTs to confirm this is indeed the behavior we want for ConvertedType in cast scenario. #Resolved

Copy link
Member

@333fred 333fred Sep 8, 2023

Choose a reason for hiding this comment

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

This is not what I would expect. I would expect the type and the converted type to be null. There is an explicit nullable conversion on top of this, with an underlying implicit collection conversion on the input to 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.

I see. Let's chat with Aleksey on Monday to confirm. If that's the case, then the previous behavior (absent nullable scenarios) was incorrect.

Copy link
Member Author

Choose a reason for hiding this comment

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

Confirmed the behavior of ConvertedType with some other typeless expression. I'll go with that.

        [Fact]
        public void TODO2()
        {
            string src = """
class Program
{
    static int? M()
    {
        return (int?)null;
    }
}
""";
            var comp = CreateCompilation(src);
            comp.VerifyDiagnostics();
            var tree = comp.SyntaxTrees.First();
            var model = comp.GetSemanticModel(tree);
            var value = tree.GetRoot().DescendantNodes().OfType<CastExpressionSyntax>().Last().Expression;
            var typeInfo = model.GetTypeInfo(value);
            Assert.Null(typeInfo.Type);
            Assert.Null(typeInfo.ConvertedType);
        }

@jcouv jcouv requested a review from 333fred September 8, 2023 22:05
@@ -2201,7 +2201,7 @@ private static bool IsUserDefinedTrueOrFalse(BoundUnaryOperator @operator)
else if (boundNodeForSyntacticParent is BoundConversion parentConversion)
{
// There was an explicit cast on top of the collection expression.
convertedType = parentConversion.Type;
convertedType = highestBoundExpr.Type;
convertedNullability = nullability;
conversion = parentConversion.Conversion;
Copy link
Member

@333fred 333fred Sep 8, 2023

Choose a reason for hiding this comment

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

This is also incorrect. #Resolved

@jcouv jcouv requested a review from 333fred September 9, 2023 00:27
@jcouv jcouv marked this pull request as ready for review September 12, 2023 20:57
@jcouv
Copy link
Member Author

jcouv commented Sep 12, 2023

@cston @333fred Please take another look.
The fixes to the semantic model affect one IDE test (broken) which Cyrus is looking at (we'll either fix or skip that test with a tracking issue). #Closed

@jcouv jcouv requested a review from cston September 12, 2023 20:58
@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner September 12, 2023 21:09
public static bool IsSystemNullable(this TypeSymbol? type, [NotNullWhen(true)] out TypeSymbol? underlyingType)
{
if (type is NamedTypeSymbol nt
&& nt.OriginalDefinition.GetSpecialTypeSafe() == SpecialType.System_Nullable_T)
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Sep 12, 2023

Choose a reason for hiding this comment

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

what's the difference between this and IsNullableType above? specifically, why GetSpecialTypeSafe() versus .SpecialType? #Resolved

Copy link
Member

Choose a reason for hiding this comment

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

should IsNullableType defer to IsSystemNullable so everything is consistent?

Copy link
Member

Choose a reason for hiding this comment

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

why do you need to check if its' a NamedType if you're already going use the SpecialTYpe?

Copy link
Member Author

Choose a reason for hiding this comment

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

what's the difference between this and IsNullableType above?

The difference is that we're returning the underlying type.

specifically, why GetSpecialTypeSafe() versus .SpecialType?

This is a refactoring of existing code. GetSpecialTypeSafe is just SpecialType plus a null check. That said, we don't need GetSpecialTypeSafe() here since OriginalDefinition cannot be null.

should IsNullableType defer to IsSystemNullable so everything is consistent?

I don't see a need.

why do you need to check if its' a NamedType if you're already going use the SpecialTYpe?

We can only access TypeArgumentsWithAnnotationsNoUseSiteDiagnostics on a NamedTypeSymbol.

Copy link
Member

Choose a reason for hiding this comment

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

My basic point is that the these functions are virtually the same, except for the convenience of getting the type argument. As such, keeping them as similar as possible (naming, impl, etc.) seems like the best thing to do. Having them be subtly different just seems like a way for bugs to creep in.

Wrt the named type check. It seems better to do the special type check first, then just do a cast afterwards.

@@ -1129,6 +1129,32 @@ private Conversion ClassifyImplicitBuiltInConversionFromExpression(BoundExpressi
return Conversion.NoConversion;
}

#nullable enable
private Conversion GetImplicitCollectionExpressionConversion(BoundUnconvertedCollectionExpression collectionExpression, TypeSymbol destination, CompoundUseSiteInfo<AssemblySymbol> useSiteInfo)
Copy link
Member

@cston cston Sep 13, 2023

Choose a reason for hiding this comment

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

CompoundUseSiteInfo

Should this be a ref parameter? #Closed

conversion = convertedCollection.CollectionTypeKind == CollectionExpressionTypeKind.None
? Conversion.NoConversion
: Conversion.CollectionExpression;
// Explicit cast or error scenario like `object x = [];`
Copy link
Member

@cston cston Sep 13, 2023

Choose a reason for hiding this comment

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

Explicit cast

Does this case still cover explicit casts? (If so, it's surprising the converted type is null and the conversion is Identity.) If not, consider updating the comment. #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, this case still covers explicit casts (test NullableValueType_ExplicitCast for instance).

The behavior for ConvertedType and the conversion matches what we have for other typeless expressions with an explicit cast:

        [Fact]
        public void TODO2()
        {
            string src = """
partial class Program
{
    static object M()
    {
        return (object)null;
    }
}
""";
            var comp = CreateCompilation(src, targetFramework: TargetFramework.Net80);
            comp.VerifyDiagnostics();

            var tree = comp.SyntaxTrees.First();
            var model = comp.GetSemanticModel(tree);

            var value = tree.GetRoot().DescendantNodes().OfType<CastExpressionSyntax>().Last().Expression;
            Assert.Equal("null", value.ToFullString());
            var conversion = model.GetConversion(value);
            Assert.True(conversion.IsIdentity);

            var typeInfo = model.GetTypeInfo(value);
            Assert.Null(typeInfo.Type);
            Assert.Null(typeInfo.ConvertedType);
        }

@@ -146,6 +146,19 @@ public static TypeSymbol GetNullableUnderlyingType(this TypeSymbol type)
return type.GetNullableUnderlyingTypeWithAnnotations().Type;
}

public static bool IsSystemNullable(this TypeSymbol? type, [NotNullWhen(true)] out TypeSymbol? underlyingType)
Copy link
Member

@cston cston Sep 13, 2023

Choose a reason for hiding this comment

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

IsSystemNullable

Perhaps rename to IsNullableType to match the methods above. #Closed


if (speculationAnalyzer.ReplacementChangesSemantics())
return false;
// HACK: Workaround lack of compiler information for collection expression conversions with casts.
Copy link
Member

Choose a reason for hiding this comment

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

HACK

Are we expecting to fix this in #68826? If so, let's reference the issue here instead.

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 let @CyrusNajmabadi answer that one, since this is part of his change.

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.

LGTM. Just a couple of nits

@@ -2851,6 +2851,91 @@ static void Main()
comp.VerifyEmitDiagnostics();
}

[Fact]
public void TypeInference_Nullable()
Copy link
Member

@333fred 333fred Sep 13, 2023

Choose a reason for hiding this comment

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

Nullable is a bit of an ambiguous term now, let's clarify what nullable we mean :).

Suggested change
public void TypeInference_Nullable()
public void TypeInference_NullableValueType()
``` #Resolved

}

[Fact]
public void TypeInference_Nullable_ExtensionMethod()
Copy link
Member

@333fred 333fred Sep 13, 2023

Choose a reason for hiding this comment

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

Suggested change
public void TypeInference_Nullable_ExtensionMethod()
public void TypeInference_NullableValueType_ExtensionMethod()
``` #Resolved

@jcouv jcouv enabled auto-merge (squash) September 13, 2023 22:38
@jcouv jcouv merged commit 7808feb into dotnet:main Sep 14, 2023
27 checks passed
@ghost ghost modified the milestones: 17.8, Next Sep 14, 2023
@Cosifne Cosifne modified the milestones: Next, 17.8 P3 Sep 25, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers New Feature - Collection Expressions 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.

Collection expressions need to work with nullable-value-target-types
5 participants