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

Do not treat Nullable null value as a constant for the purpose of IL generation. #64789

Merged
merged 1 commit into from
Oct 18, 2022

Conversation

AlekseyTs
Copy link
Contributor

Fixes #64777.

@AlekseyTs AlekseyTs requested a review from a team as a code owner October 18, 2022 00:06
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()))
Copy link
Member

@jcouv jcouv Oct 18, 2022

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

Copy link
Member

@jcouv jcouv Oct 18, 2022

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?

Copy link
Contributor Author

@AlekseyTs AlekseyTs Oct 18, 2022

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.

Copy link
Member

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).

Copy link
Contributor Author

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

Copy link
Contributor Author

@AlekseyTs AlekseyTs Oct 18, 2022

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?

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.

Copy link
Member

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

@jcouv jcouv self-assigned this Oct 18, 2022
Copy link
Member

@jcouv jcouv left a 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)

@AlekseyTs
Copy link
Contributor Author

@dotnet/roslyn-compiler For the second review.

@AlekseyTs AlekseyTs merged commit 219d303 into dotnet:main Oct 18, 2022
@ghost ghost added this to the Next milestone Oct 18, 2022
@RikkiGibson RikkiGibson modified the milestones: Next, 17.5 P1 Oct 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants