-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Do not treat Nullable
null value as a constant for the purpose of IL generation.
#64789
Conversation
…L generation. Fixes dotnet#64777.
if ((object)expression.Type == null || expression.Type.SpecialType != SpecialType.System_Decimal) | ||
if ((object)expression.Type == null || | ||
(expression.Type.SpecialType != SpecialType.System_Decimal && | ||
!expression.Type.IsNullableType())) |
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 need a bit of help to understand this fix (it seems in a very central location and surprising).
From what I can tell, the problem is only occurring when there is an identity conversion between the implicit user-defined conversion and the null
literal.
Below are the bound tree for nullable tuples scenario (broken) vs. nullable int
scenario (works) for comparison.
Looking at those two trees, I can see that there is an additional identity conversion in the tuples scenario. It's a conversion of default
to type (int SomeInt, bool SomeBool)?
.
Why does that conversion node have a constant value, but the child default
node doesn't?
// tuples
statementList
└─statements
└─block
└─statements
└─sequencePoint
└─statementOpt
└─returnStatement
├─refKind: None
├─expressionOpt
│ └─call
│ ├─receiverOpt
│ ├─method: ImplicitConversionTargetType<(int SomeInt, bool SomeBool)?>.implicit operator ImplicitConversionTargetType<(int SomeInt, bool SomeBool)?>((int SomeInt, bool SomeBool)?)
│ ├─arguments
│ │ └─conversion
│ │ ├─operand
│ │ │ └─defaultExpression
│ │ │ ├─targetType
│ │ │ ├─constantValueOpt
│ │ │ └─type: (int, bool)?
│ │ ├─conversion: Identity
│ │ ├─isBaseConversion: False
│ │ ├─@checked: False
│ │ ├─explicitCastInCode: False
│ │ ├─constantValueOpt: ConstantValueNull(null: Nothing) // <--- this seems questionable
│ │ ├─conversionGroupOpt: Microsoft.CodeAnalysis.CSharp.ConversionGroup
│ │ ├─originalUserDefinedConversionsOpt: {}
│ │ └─type: (int SomeInt, bool SomeBool)?
│ ├─argumentNamesOpt: (null)
│ ├─argumentRefKindsOpt: (null)
│ ├─isDelegateCall: False
│ ├─expanded: False
│ ├─invokedAsExtensionMethod: False
│ ├─argsToParamsOpt: (null)
│ ├─defaultArguments: Microsoft.CodeAnalysis.BitVector
│ ├─resultKind: Viable
│ ├─originalMethodsOpt: (null)
│ └─type: ImplicitConversionTargetType<(int SomeInt, bool SomeBool)?>
└─@checked: False
// int
statementList
└─statements
└─block
└─statements
└─sequencePoint
└─statementOpt
└─returnStatement
├─refKind: None
├─expressionOpt
│ └─call
│ ├─receiverOpt
│ ├─method: ImplicitConversionTargetType<int?>.implicit operator ImplicitConversionTargetType<int?>(int?)
│ ├─arguments
│ │ └─defaultExpression
│ │ ├─targetType
│ │ ├─constantValueOpt
│ │ └─type: int?
│ ├─argumentNamesOpt: (null)
│ ├─argumentRefKindsOpt: (null)
│ ├─isDelegateCall: False
│ ├─expanded: False
│ ├─invokedAsExtensionMethod: False
│ ├─argsToParamsOpt: (null)
│ ├─defaultArguments: Microsoft.CodeAnalysis.BitVector
│ ├─resultKind: Viable
│ ├─originalMethodsOpt: (null)
│ └─type: ImplicitConversionTargetType<int?>
└─@checked: False
#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.
Looking at the spec (constants), the identity conversion of null
should be a constant. That suggests the conversion node is correct (has constantValueOpt
) but the default expression nodes are missing constantValueOpt
. Should we file an issue on that?
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.
Why does that conversion node have a constant value, but the child default node doesn't?
Because, strictly speaking, the original conversion node, created by initial binding, shouldn't have one either. In my opinion this an old bug in compiler's behavior, see #24380. Unfortunately, the presence of the constant value on the conversion was taken advantage of in implementation of some features (patterns, for example) and there was a resistance to get this cleaned up. Given the situation, various parts of the compiler have to compensate for that. For example, LocalRewriter has this code to ignore nullable constant values:
private BoundExpression? VisitExpressionImpl(BoundExpression node)
{
ConstantValue? constantValue = node.ConstantValue;
if (constantValue != null)
{
TypeSymbol? type = node.Type;
if (type?.IsNullableType() != true)
{
return MakeLiteral(node.Syntax, constantValue, type);
}
}
The proposed fix is essentially doing the same thing in IL generation. If we get consensus around fixing the #24380, we can do that during MQ milestone.
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.
Thanks for the clarification.
The link you shared might be incorrect though (seems like an unrelated merged PR).
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 link you shared might be incorrect though (seems like an unrelated merged PR).
The first link is correct, the second reference is missing zero at the end, I'll fix
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.
Looking at the spec (constants), the identity conversion of
null
should be a constant. That suggests the conversion node is correct (hasconstantValueOpt
) but the default expression nodes are missingconstantValueOpt
. Should we file an issue on that?
I actually disagree with this analysis and think that, according to the specification, values of a nullable type should never have a constant value. This applies to a nullable null, and to a nullable default value. I think I expressed the rationale behind this opinion in #24380.
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.
values of a nullable type should never have a constant value
Reading the thread in #24380, I'm convinced. The allowed conversions for constants are enumerated, but the conversion of null
to nullable value type (described here) isn't listed.
Hopefully we have a special rule somewhere for default parameter values void M(int? i = null)
, since this is not quite a constant.
Then from an implementation perspective, the constant value (non-null constantValueOpt
) indeed would be the root cause. I'm still okay with localized workaround for now. 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.
LGTM Thanks (iteration 1)
@dotnet/roslyn-compiler For the second review. |
Fixes #64777.