- 
                Notifications
    
You must be signed in to change notification settings  - Fork 4.2k
 
Fix Color Color const field initialization accessing the Color type #68283
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
base: main
Are you sure you want to change the base?
Conversation
| // void f() { if () const int i = 0; } | ||
| Diagnostic(ErrorCode.WRN_UnreferencedLocalFunction, "f").WithArguments("f").WithLocation(6, 14) | ||
| ); | ||
| Diagnostic(ErrorCode.WRN_UnreferencedLocalFunction, "f").WithArguments("f").WithLocation(6, 14)); | 
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.
| ); | ||
| // (6,51): error CS0133: The expression being assigned to 'b' must be constant | ||
| // const Func<int> a = () => { const int b = a(); return 1; }; | ||
| Diagnostic(ErrorCode.ERR_NotConstantExpression, "a()").WithArguments("b").WithLocation(6, 51)); | 
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.
| } | ||
| 
               | 
          ||
| var memberAccessParent = left.Parent as MemberAccessExpressionSyntax; | ||
| Debug.Assert(memberAccessParent is not 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.
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.
Do you think we can trigger this via a pointer access expression?
| } | ||
| 
               | 
          ||
| // The first in container binder reflects the current scope, so we begin at the outer scope of the binder | ||
| for (var binder = firstOuterScopeBinder; binder is not null; binder = binder.Next) | 
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.
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.
We find the first binder that has a broader scope than ours that contains the bound const field. Through observation I figured that we want to find the container type that the binder is associated with, meaning that we expand our lookup into better candidate symbols.
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.
Through observation I figured that we want to find the container type that the binder is associated with, meaning that we expand our lookup into better candidate symbols.
I think, we should not change anything about lookup here, and therefore, shouldn't be changing binders. We also do not need to rebind anything because the meaning of the identifier should not change. It appears that the code does change the meaning and that is wrong. If I understand correctly, the goal is to "circumvent" the ConstantFieldsInProgress tracking. We should be looking for a different way to do that.
| for (var binder = firstOuterScopeBinder; binder is not null; binder = binder.Next) | ||
| { | ||
| // While trying to find a suitable binder for the expression, we do not report diagnostics | ||
| targetBoundValue = binder.BindMemberAccess(memberAccessParent, false, false, BindingDiagnosticBag.Discarded); | 
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.
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.
One case that I could theorize could break this involves nested classes containing visible const fields each. This sure sounds interesting to include as a test case.
| Debug.Assert(!leftType.IsDynamic()); | ||
| Debug.Assert(IsPotentialColorColorReceiver(left, leftType)); | ||
| 
               | 
          ||
| if (leftSymbol is SourceFieldSymbolWithSyntaxReference fieldLeft) | 
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.
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 that is because ConstantFieldsInProgress tracks only these symbols.
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.
However, it is not obvious to me that all affected scenarios are going to be handled if we apply this condition here.
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 only symbols that encounter this bug are fields, and this is the only type that is being tracked by ConstantFieldsInProgress as you said. What kind of affected scenarios are you considering could be missed?
| { | ||
| boundValue = RebindColorColorConstField(left, boundValue, fieldLeft); | ||
| leftSymbol = boundValue.ExpressionSymbol; | ||
| ConstantFieldsInProgress.RemoveDepencency(fieldLeft); | 
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.
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 is deeper than I thought; the more the hours were passing, the more convinced I am that we need to approve a spec change, which will make total sense for this line of code.
Essentially, on top of the field initialization, we could entirely exclude the field itself that is being initialized, in order to properly bind to another symbol like a namespace, or type. I will open up a discussion/issue for this matter following further contact.
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.
Essentially, on top of the field initialization, we could entirely exclude the field itself that is being initialized, in order to properly bind to another symbol like a namespace, or type. I will open up a discussion/issue for this matter following further contact.
I think changing the spec is premature. Even if this particular attempt to address the issue isn't something we are comfortable with, it doesn't mean there is no alternative way to approach the problem. We can "sit" on the issue a little longer and let someone from the compiler team to take a closer look once we have time.
| 
           Done with review pass (commit 2)  | 
    
| 
           @Rekkonnect Just in case, please do not do a forced push to a PR under review.  | 
    
| 
               | 
          ||
| // We want to avoid binding to the const field symbol, | ||
| // so we break on the first symbol that does not equal that | ||
| if (!boundValue.ExpressionSymbol.Equals(targetBoundValue.ExpressionSymbol)) | 
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.
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 know you could reliably bound field expressions, perhaps I could try that out
| if (leftSymbol is SourceFieldSymbolWithSyntaxReference fieldLeft) | ||
| { | ||
| boundValue = RebindColorColorConstField(left, boundValue, fieldLeft); | ||
| leftSymbol = boundValue.ExpressionSymbol; | 
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.
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.
We do rebind the symbol, and upon returning the bound expression a few lines below this, we also provide the left symbol that is the dependency for this constant's dependency graph, resolving the issue with the circular reference. After having successfully rebound the symbol, we avoid introducing the field itself into the dependency graph as well.
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.
We do rebind the symbol, and upon returning the bound expression a few lines below this, we also provide the left symbol that is the dependency for this constant's dependency graph, resolving the issue with the circular reference. After having successfully rebound the symbol, we avoid introducing the field itself into the dependency graph as well.
This doesn't answer the question: "Do we expect the symbol to change?"
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.
More specifically. Why is this the right thing to replace the field symbol with a symbol that the enclosing member access resolves to?
| } | ||
| } | ||
| 
               | 
          ||
| return targetBoundValue; | 
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.
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 is the exact logic, I thought I described that in the summary of the PR. The point is to rebind to a visible member that is not the constant field itself that is being accessed, because under the current circumstances, you can never yield a constant expression when accessing a constant expression's member.
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 is the exact logic, I thought I described that in the summary of the PR
Well, replacing result of binding an identifier with result of binding the enclosing member access is not the right thing to do in my opinion. Whether described or not.
| for (var binder = firstOuterScopeBinder; binder is not null; binder = binder.Next) | ||
| { | ||
| // While trying to find a suitable binder for the expression, we do not report diagnostics | ||
| targetBoundValue = binder.BindMemberAccess(memberAccessParent, false, false, BindingDiagnosticBag.Discarded); | 
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.
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.
Because I think we will never fall into the case of rebinding the LHS of a MAE that is a constant field, if the expression is indexed or invoked. But I'm not entirely sure about that either.
        
          
                src/Compilers/CSharp/Test/Semantic/Semantics/ColorColorTests.cs
              
                Outdated
          
            Show resolved
            Hide resolved
        
      | 
           As discussed in dotnet/csharplang#7222, this PR will only focus on fixing the   | 
    
          
 I am not sure how to interpret this. Also, we are not making compiler changes base on dotnet/csharplang discussions. In my opinion, the language rules are fine and there is no need to change them. There is an implementation flaw in the way constants are handled in Color Color scenarios, there is an issue that tracks fixing the flaw. The approach taken in this PR to address the flaw is wrong, in my opinion. I suggest letting compiler team to tackle the issue, unless you have an alternative in mind.  | 
    
| 
           I can understand the approach being wrong or clunky, it doesn't completely resonate too sanely in my mind either. What I meant by not handling namespace binding in this PR is that the spec does not permit binding a  The purpose of this PR is to fix the bug, according to the current spec. As it stands, const field initializers and enum field initializers should not cause this error, meaning that the  There might be some language barrier in interpreting my previous comment, hope this comment clears this out.  | 
    
| 
           @AlekseyTs from offline discussion with Rekkonect, there is an understanding that the only 'issue' under consideration here is around the  I think he was simply stating that other things he noticed while looking into this are not actually related to this topic and are out of scope from this actually identified potential issue with constants.  | 
    
| 
           Done with review pass (commit 6)  | 
    
| 
           @Rekkonnect It looks like there are legitimate test failures  | 
    
| 
           This specific test that failed passes just fine in me. Could it be some pooling bug when running all the tests? I only ran a small subset of related tests to make sure the feature passes, but failed to cause the NRE.  | 
    
| 
           Judging by the call stack, the crash looks primarily related to the changes made in this PR: It looks like  The "Test_Windows_CoreClr_IOperation_Debug" CI leg executes extra validation for test scenarios. You should be able to run the test locally in the same mode if you temporarily comment out an  in src/Compilers/Test/Core/Compilation/CompilationExtensions.cs. #Closed  | 
    
| 
           Thank you for your elaborate guidance, I will have a look when I have the time and have it fixed for that scenario  | 
    
| 
           Moving to Backlog due to inactivity  | 
    
| 
           @AlekseyTs the test case was fixed and all checks pass now, I think the PR is ready for a final review  | 
    
| 
           @AlekseyTs ptal  | 
    
| 
           @AlekseyTs this is ready for review  | 
    
| private readonly SourceFieldSymbol _fieldOpt; | ||
| private readonly HashSet<SourceFieldSymbolWithSyntaxReference> _dependencies; | ||
| 
               | 
          ||
| private SourceFieldSymbolWithSyntaxReference _recentDependency; | 
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.
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.
Added comment
| _recentDependency = field; | ||
| } | ||
| 
               | 
          ||
| internal void RemoveDepencency(SourceFieldSymbolWithSyntaxReference field) | 
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.
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.
Removed as unused after inlining
| 
               | 
          ||
| private bool EqualsRecentDependency(SourceFieldSymbolWithSyntaxReference field) | ||
| { | ||
| return _recentDependency == field; | 
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.
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.
Added (object) cast
| 
               | 
          ||
| internal void RemoveIfEqualsRecentDependency(SourceFieldSymbolWithSyntaxReference field) | ||
| { | ||
| if (EqualsRecentDependency(field)) | 
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.
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.
Inlined
| { | ||
| if (EqualsRecentDependency(field)) | ||
| { | ||
| RemoveRecentDependency(); | 
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.
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.
Inlined
| 
           @AlekseyTs changed the code, ptal  | 
    
| using Roslyn.Utilities; | ||
| 
               | 
          ||
| namespace Microsoft.CodeAnalysis.CSharp | ||
| namespace Microsoft.CodeAnalysis.CSharp; | 
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.
Style changes like that are not welcome under Compilers, please revert. #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.
Reverted to block-scoped
| 
               | 
          ||
| if (leftSymbol is SourceFieldSymbolWithSyntaxReference fieldLeft) | ||
| { | ||
| ConstantFieldsInProgress.RemoveIfLastDependency(fieldLeft); | 
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 still find the approach to adjust/fixup the ConstantFieldsInProgress set fragile and bug prone. For example, it doesn't look like current implementation properly handles the following scenario:
public const Color Color = Color | Color.Red;
A cycle should be detected. Instead emit fails with the following error:
error CS7038: Failed to emit module '...': Unable to determine specific cause of the failure.
I prefer an alternative approach that avoids premature binding of the value. It is implemented in #80978.
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 like your PR more than this one, it does the job more cleanly. We can close this one out unless the other PR doesn't go anywhere
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.
Let's wait for it to go through code review process
| 
           Done with review pass (commit 15)  | 
    
Closes #45739
Summary
Color Color cases of const field initializations would trigger a circular dependency error. This bug was being caused in the constant initializer dependency graph calculation. Another contributor was the binder resolving
Coloras the field, rather than the type itself.Details
When binding inside
BindLeftIdentifierOfPotentialColorColorMemberAccessinBinder_Expressions.cs, we also remove the dependency of the const field itself from the graph, if it is part of the dependencies, which in turn eliminates the circular dependency error. This came with the introduction of aRemoveIfEqualsRecentDependencymethod inConstantFieldsInProgress, to directly remove the dependency from the set that was passed down during the evaluation of the field's constant initialization, if it was the recent dependency in the graph, which is also tracked down upon adding the dependency.Example
As added in the test, the following simple case determines the existence of the bug:
Color.Redwas previously referring to the const fieldColor, rather than the enumColor. This caused a viable circular dependency error, due to the mistaken binding.Now,
Color.Redis resolved as accessing theRedmember of theColorenum type.