-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Initial support for nullable analysis for Unions #81721
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
Conversation
|
@333fred, @RikkiGibson, @dotnet/roslyn-compiler Please review |
|
|
||
| if (CheckDisallowedNullAssignment(operandType, parameterAnnotations, conversionOperand.Syntax)) | ||
| { | ||
| LearnFromNonNullTest(conversionOperand, ref State); |
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.
333fred
left a comment
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 review pass (commit 1)
|
@dotnet/roslyn-compiler For a second review |
2 similar comments
|
@dotnet/roslyn-compiler For a second review |
|
@dotnet/roslyn-compiler For a second review |
| return property is | ||
| { | ||
| IsStatic: false, | ||
| DeclaredAccessibility: Accessibility.Public, |
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.
just checking, it's intentional that this is public, and not just accessible to the caller? #Resolved
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.
Yes it is intentional. Definition of the interface from the spec:
public interface IUnion
{
// The value of the union or null
object? Value { get; }
}
BTW, this is not a new code, a member method got simply changed to a local function
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.
Yes it is intentional. Definition of the interface from the spec:
Thanks :)
BTW, this is not a new code, a member method got simply changed to a local function
Understood. It was still something that i wanted to double check :)
| DeclaredAccessibility: Accessibility.Public, | ||
| IsAbstract: true, | ||
| GetMethod: not null, | ||
| SetMethod: null, |
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.
intentional that it definitely has no set method, not that it doesn't matter if it does/doesn't have it? #Resolved
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.
intentional that it definitely has no set method, not that it doesn't matter if it does/doesn't have it?
Same response, intentional ,,,
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.
Ok. This part was a bit odd to me though. it wasn't clear that a member that is a superset of what is needed is inadmissible. IMO, this would be worth bringing to an email/ldm for clarity. I'm totally fine if the resolution is "no, it can't have a setter". but i def want to know if that's the intent, vs "we don't care if it has a setter or not".
| _variables[containingSlot].Symbol.GetTypeOrReturnType().Type is NamedTypeSymbol { IsUnionTypeNoUseSiteDiagnostics: true, UnionCaseTypes: not [] } unionType && | ||
| Binder.GetUnionTypeValuePropertyNoUseSiteDiagnostics(unionType) == (object)property: | ||
| { | ||
| // For union types where none of the case types are nullable, the default state for Value is "not null" rather than "maybe null". |
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 only referring to 'nullable' for nullable-reference-types, or does this also encompass nullable value types? tahnks. #Resolved
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 only referring to 'nullable' for nullable-reference-types, or does this also encompass nullable value types?
This is referring to nullable in terms of nullable analysis, and covers both kinds of types. BTW, this quotes the feature spec.
| { | ||
| SetState(ref this.State, valueSlot, operandState); | ||
| } | ||
| } |
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'm curious if this code could be shared with the similar code on like 4276 #Resolved
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'm curious if this code could be shared with the similar code on lik
I will evaluate the possibility while working on the following changes
| if (valueSlot > 0) | ||
| { | ||
| SetState(ref this.State, valueSlot, operandState); | ||
| } |
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.
similar question here. there seems to be a common pattern of reading the operand state, making a new slot, and then storing state. #Resolved
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.
similar question here.
The next change in the pipeline moves this code into VisitUnionConversion and it gets shared across the callers this way.
| { | ||
| TrackNullableStateOfTupleConversion(conversionOpt, conversionOperand, conversion, targetType, operandType.Type, slot, valueSlot, assignmentKind, parameterOpt, reportWarnings: reportRemainingWarnings); | ||
| int slot = GetOrCreatePlaceholderSlot(conversionOpt); | ||
| if (slot > 0) |
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.
should this (and the valueslot check above) have assertions that these are greater than 0? or is tehre an expectation that you could get 0/negative? if so, perhaps comment that. #Resolved
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.
should this ...
This is an existing code and I am not planning to add asserts or comments to it.
|
Haven't looked at tests ye. want i want to better understnd this null walking code first. |
|
@RikkiGibson, @dotnet/roslyn-compiler For a second review |
| var discardedUseSiteInfo = CompoundUseSiteInfo<AssemblySymbol>.Discarded; | ||
| return HasTopLevelNullabilityImplicitConversion(source, destination) && | ||
| ClassifyImplicitConversionFromType(source.Type, destination.Type, ref discardedUseSiteInfo).Kind != ConversionKind.NoConversion; | ||
| Conversion conversion = ClassifyImplicitConversionFromType(source.Type, destination.Type, ref discardedUseSiteInfo); |
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 know this didn't change. but so i understand better. what's the logic used to determine if the use site info can/should be discarded, vs having the caller of this pass in info to fill in that data? #Resolved
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 know this didn't change. but so i understand better. what's the logic used to determine if the use site info can/should be discarded, vs having the caller of this pass in info to fill in that data?
I do not know if this answer is going to help much, but the decision is usually made based on the planned/expected usage pattern for the API and whether we will be interested in the info in the callers.
| ClassifyImplicitConversionFromType(source.Type, destination.Type, ref discardedUseSiteInfo).Kind != ConversionKind.NoConversion; | ||
| Conversion conversion = ClassifyImplicitConversionFromType(source.Type, destination.Type, ref discardedUseSiteInfo); | ||
| return conversion.Kind != ConversionKind.NoConversion && | ||
| (conversion.IsUnion || conversion.IsUserDefined || HasTopLevelNullabilityImplicitConversion(source, 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.
i guess i'm not understanding why having a conversion be a union or user-defined means that there are "any nullability implicit conversion". Does this fall out from something in the spec? something else? #Resolved
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 guess i'm not understanding why having a conversion be a union or user-defined means that there are "any nullability implicit conversion". Does this fall out from something in the spec? something else?
The logic in HasTopLevelNullabilityImplicitConversion doesn't look appropriate for user-defined and union conversions because nullability doesn't transfer directly from source to result in these cases. The change was prompted by new tests that I added.
| if (!conversions.HasTopLevelNullabilityImplicitConversion(s, d)) | ||
| Conversion conversion = conversions.ClassifyImplicitConversionFromType(s.Type, d.Type, ref u); | ||
|
|
||
| if (!conversion.IsUserDefined && !conversion.IsUnion && !conversions.HasTopLevelNullabilityImplicitConversion(s, d)) |
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 check seems to be the negation of the check done in HasAnyNullabilityImplicitConversion. Would it make sense to extract to some named helper that both could use that would then be clearer as to why it is the right check in both locations? #Resolved
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 check seems to be the negation of the check done in HasAnyNullabilityImplicitConversion. Would it make sense to extract to some named helper that both could use that would then be clearer as to why it is the right check in both locations?
I do not think this is worthwhile and not planning doing that. There are other call sites for HasTopLevelNullabilityImplicitConversion
| if (!conversions.HasTopLevelNullabilityImplicitConversion(s, d)) | ||
| Conversion conversion = conversions.ClassifyConversionFromType(s.Type, d.Type, isChecked: isChecked, ref u, forCast); | ||
|
|
||
| if (!conversion.IsUserDefined && !conversion.IsUnion && !conversions.HasTopLevelNullabilityImplicitConversion(s, d)) |
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.
same concern here. doing the same check in 3 places def makes it seem worthwhile to extract out. #Resolved
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.
same concern here. doing the same check in 3 places def makes it seem worthwhile to extract out.
I have no plans doing that
|
|
||
| return conversions.ClassifyImplicitConversionFromTypeWhenNeitherOrBothFunctionTypes(source, destination, ref useSiteInfo).Exists; | ||
| Conversion conversion = conversions.ClassifyImplicitConversionFromTypeWhenNeitherOrBothFunctionTypes(source, destination, ref useSiteInfo); | ||
| return conversion.Exists && (conversion.IsUnion || conversion.IsUserDefined || conversions.HasTopLevelNullabilityImplicitConversion(sourceWithAnnotations, destinationWithAnnotations)); |
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.
here as well. #WontFix
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.
having the same logic in 4 places def seems like something worth extracting. especially since, frm my readin ghere, if we updated one, we'd want to update all 4 of these.
| case PropertySymbol { Name: WellKnownMemberNames.ValuePropertyName } property when | ||
| variable.ContainingSlot is > 0 and var containingSlot && | ||
| property.ContainingType.IsWellKnownTypeIUnion() && | ||
| _variables[containingSlot].Symbol.GetTypeOrReturnType().Type is NamedTypeSymbol { IsUnionTypeNoUseSiteDiagnostics: true, UnionCaseTypes: not [] } unionType && |
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.
interesting to have the not [] check there. Does a union require at least one case-type? (this is not a comment about the code, just trying to wrap my head around how the language views things). #Resolved
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.
just trying to wrap my head around how the language views things
There is nothing interesting we can possibly do for a union type without case types.
| conversionsWithoutNullability.HasIdentityOrImplicitReferenceConversion(possibleDerived, possibleBase, ref discardedUseSiteInfo) || | ||
| conversionsWithoutNullability.HasBoxingConversion(possibleDerived, possibleBase, ref discardedUseSiteInfo); | ||
| conversionsWithoutNullability.HasBoxingConversion(possibleDerived, possibleBase, ref discardedUseSiteInfo) || | ||
| (possibleBase.IsInterfaceType() && conversionsWithoutNullability.HasImplicitConversionToOrImplementsVarianceCompatibleInterface(possibleDerived, (NamedTypeSymbol)possibleBase, ref discardedUseSiteInfo, needSupportForRefStructInterfaces: out _)); |
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'm not really understanding the purpose/impact of this code here. Would it be possible to break this up into individual checks, with an explanation of why this case is not a slot member? #Resolved
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'm not really understanding the purpose/impact of this code here. Would it be possible to break this up into individual checks, with an explanation of why this case is not a slot member?
This change isn't union specific. Ref structs implementing interfaces cannot be boxed. Therefore, the previous check for HasBoxingConversion fails for them even when a ref struct implements the interface. This change simply patches the condition to properly handle ref struct scenarios.
| var resultState = NullableFlowState.NotNull; | ||
| if (type is object && | ||
| (hasObjectInitializer || type.IsStructType())) | ||
| (hasObjectInitializer || type.IsStructType() || isSuitableUnionConstruction(type, constructor, out _))) |
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.
rthis is a bit interesting. it seems like we'll almost always call into this a second time below. so maybe it would make sense to just life this out and always call it, always assigning to out PropertySymbol? valueProperty. Then, inside, you can just check if htat value is non-null to go into the innermost if-block. #Resolved
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.
Then, inside, you can just check if htat value is non-null to go into the innermost if-block.
I am more comfortable with the current code flow for now.
| } | ||
| else | ||
| { | ||
| operandState = argumentResults[0].RValueType.State; |
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'm curious why this is the right operand state if the operand slot was <= 0. #Resolved
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'm curious why this is the right operand state if the operand slot was <= 0.
If we don't have a slot for the argument, this is the only information we have for its state.
| constructor.ContainingType.Equals(type, TypeCompareKind.AllIgnoreOptions) && | ||
| type is NamedTypeSymbol { IsUnionTypeNoUseSiteDiagnostics: true } unionType && | ||
| NamedTypeSymbol.IsSuitableUnionConstructor(constructor) && | ||
| (valueProperty = Binder.GetUnionTypeValuePropertyNoUseSiteDiagnostics(unionType)) is { }; |
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.
fwiw, doing the assignment/check to the out param within this complex epression was difficult to discover/understand. I think breaking into a simple assignment and null check would be clearer and more idiomatic. #Resolved
| static void Test5(bool? x) | ||
| { | ||
| #line 500 |
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.
the use of #line is helpful. thanks. #Resolved
| [CombinatorialData] | ||
| public void NullableAnalysis_05_State_From_Constructor([CombinatorialValues("class", "struct")] string typeKind) | ||
| { | ||
| var src = @" |
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.
curious why concat instead of string (or raw string) interpolation. Parsing this out def took more mental effort for me :) #Resolved
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.
curious why concat instead of string (or raw string) interpolation.
Personal preference
| static void Test2() | ||
| { | ||
| #line 200 | ||
| var s = new S1(""""); |
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.
raw strings would make internal string literals easier to reason about when reviewing. tnx. #WontFix
| } | ||
|
|
||
| [Fact] | ||
| public void NullableAnalysis_19_State_From_Null_Test() |
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.
reached this test. pausing.
RikkiGibson
left a comment
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, comment is not blocking.
| Debug.Assert(conversionOperandSlot > 0); | ||
| } | ||
|
|
||
| if (conversionOperandSlot > 0 && valueFieldSlot > 0) |
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 didn't follow why we only want to perform the below SetState call when valueFieldSlot > 0. I don't think there is any test in UnionsTests which fails when the condition is removed.
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 didn't follow why we only want to perform the below SetState call when valueFieldSlot > 0.
Probably the thinking was that setting the state is not really useful otherwise. Only when valueFieldSlot > 0 the valueFieldType.State is actually coming from this.State. I am changing this code significantly in the next PR and this logic is changing as well.
No description provided.