-
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
Change local function definite assignment #23749
Change local function definite assignment #23749
Conversation
The LDM has decided that the following rules should be in effect when calculating definite assignment for local functions: 1. The entry point to a local function is always reachable. 2. Variables captured in local functions are definitely assigned if they are definitely assigned in all branches into the local function. It turns out these rules were almost exactly what the compiler already implemented, but there was a bug in captured variable detection that meant that variables captured in lambdas within local functions were sometimes not counted as captured. This change fixes the bug around capturing, which should cause the compiler to conform to this specification. Fixes dotnet#17829
cc @jaredpar |
Please document the breaking change (new diagnostics can be produced). |
// Walk up the containing symbols until we find the target function, in which | ||
// case the variable is not captured by the target function, or null, in which | ||
// case it is. | ||
var currentFunction = variable.ContainingSymbol; |
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 could be
for (var currentFunction = variable.ContainingSymbol; currentFunction != null; currentFunction = currentFunction.ContainingSymbol)
{
if (currentFunction == containingMethodOrLambda)
{
return false;
}
}
// on a particular branch. | ||
// Assignments to captured variables are also recorded, as a local function | ||
// definitely assigns captured variables on a call to a local function | ||
// if that variable is definitely assigned at all branches out of the |
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.
branches out of [](start = 62, length = 15)
"branches out of" should be "returns from". #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.
Yield return, await, and the implicit return before the first statement in an iterator all count -- would you classify those as return statements?
// Local functions don't affect outer state and are analyzed | ||
// with everything unassigned and reachable | ||
// SPEC: The entry point to a local function is always reachable. | ||
// Captured variables are assigned if they are assigned on all |
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.
assigned [](start = 38, length = 8)
"assigned" should be "definitely assigned" (twice). #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.
@jcouv Where do we document breaking changes now? |
ping @dotnet/roslyn-compiler for a second review @jaredpar @MeiChin-Tsai for ask-mode |
Approved. Is there a way to assess how breaking this is? thx. |
@@ -4,3 +4,19 @@ | |||
Each entry should include a short description of the break, followed by either a link to the issue describing the full details of the break or the full details of the break inline.* | |||
|
|||
1. Previously, we would not find a best type among multiple types (for ternary expressions and method type inference) when the types differed only in dynamic-ness and would involve some nesting. For example, we could not find a best type between `KeyValuePair<dynamic, object>` and `KeyValuePair<object, dynamic>`. Starting with VS 2017, we can find a best type (we merge dynamic-ness). In this example, `KeyValuePair<dynamic, dynamic>` is the best type. This can affect overload resolution. For instance, allowing a better overload to be picked, which was previously discarded. See issues [#12585](https://github.com/dotnet/roslyn/issues/12585), [#14247](https://github.com/dotnet/roslyn/issues/14247), [#14213](https://github.com/dotnet/roslyn/issues/14213), and their corresponding PRs for more details. | |||
|
|||
2. VS 2017 15.0-15.5 shipped with a bug around definite assignment of local functions that did not produce definite assignment errors when an uncalled local function contains a nested lambda which captures a variable. For example: |
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.
VS 2017 15.0-15.5 [](start = 2, length = 18)
Branding guidelines: Visual Studio 2017 version 15.0-15.5
} | ||
} | ||
``` | ||
This is changed to now produce an error that the variable is not definitely assigned. |
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 wrong file. Look at the one called "post 2017"
} | ||
} | ||
``` | ||
This is changed to now produce an error that the variable is not definitely assigned. |
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.
In ... 15.6, this changed to produce...
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 think we should make the specific version change later. There's no guarantee this actually ships in 15.6.
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 PR is for master branch, which is 15.6, and you are about to merge...
@MeiChin-Tsai This change is only breaking if:
We think this is a very rare case and if it does occur, the fix is simple (delete the local function, since it is never called). |
@agocke got it. Thank you! |
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
@@ -79,3 +79,19 @@ Example: `Func<int> f = default(TypedReference).GetHashCode; // new error CS0123 | |||
- https://github.com/dotnet/roslyn/pull/23416 Before Visual Studio 2017 version 15.6 (Roslyn version 2.8) the compiler accepted `__arglist(...)` expressions with void-typed arguments. For instance, `__arglist(Console.WriteLine())`. But such program would fail at runtime. In Visual Studio 2017 version 15.6, this causes a compile-time error. | |||
|
|||
- https://github.com/dotnet/roslyn/pull/24023 In Visual Studio 2017 version 15.6, Microsoft.CodeAnalysis.CSharp.Syntax.CrefParameterSyntax constructor and Update(), the parameter refOrOutKeyword was renamed to refKindKeyword (source breaking change if you're using named arguments). | |||
|
|||
- Visual Studio 2017 15.0-15.5 shipped with a bug around definite assignment of local functions that did not produce definite assignment errors when an uncalled local function contains a nested lambda which captures a variable. For example: |
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.
which captures a variable [](start = 201, length = 25)
Is the wording here accurate? It doesn't look like the lambda in example captures a variable, it declares one and uses its value before the variable is assigned.
// case it is. | ||
for (var currentFunction = variable.ContainingSymbol; | ||
currentFunction != null; | ||
currentFunction = currentFunction.ContainingSymbol) |
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.
currentFunction = currentFunction.ContainingSymbol [](start = 17, length = 50)
Is there a reason to keep climbing through types and namespaces?
Customer scenario
The LDM has decided that the following rules should be in effect when
calculating definite assignment for local functions:
they are definitely assigned in all branches into the local function.
It turns out these rules were almost exactly what the compiler already
implemented, but there was a bug in captured variable detection that
meant that variables captured in lambdas within local functions were
sometimes not counted as captured. This change fixes the bug around
capturing, which should cause the compiler to conform to this
specification.
Bugs this fixes
Fixes #17829
Workarounds, if any
Language change. If new errors are reported due to the language change, the containing local function can simply be deleted since it must be unreachable for this code to run. In addition, a warning about an unused local function should already be produced, so diagnostics are already produced for this code.
Risk
This is a simple change to capturing, closer to a bug fix than a design change.
Performance impact
Low. This is a change from a constant-time check to a linear check of containing symbols, but
the level of nesting of local functions should be small enough that performance should not
matter.
Is this a regression from a previous update?
No, this is a design change/bug fix to something that has been present since VS2017 shipped.
Root cause analysis
This is a very subtle case that can only happen with a nested lambda inside a local function
where that local function is unreachable.
How was the bug found?
Customer reported.