-
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
Clear hoisted locals when disposing iterator and async-iterator state machines #75908
Conversation
3826321
to
9bb50d5
Compare
|
||
protected override BoundStatement GenerateHoistedLocalsCleanup(ImmutableArray<StateMachineFieldSymbol> hoistedLocals) | ||
{ | ||
// We need to clean nested hoisted local variables too (not just top-level ones) |
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 need to clean nested hoisted local variables too (not just top-level ones)
This isn't obvious. It looks like there are two call sites of this method and it is not clear why we want to have this behavior for all of them. Instead of completely ignoring the passed argument, consider adjusting the call sites to construct the right set of locals to clear. We can discuss offline in more details. #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.
This behavior could be a source of bugs in the future, since the method doesn't do what a reasonable dev would expect it to do.
src/Compilers/CSharp/Portable/Lowering/StateMachineRewriter/MethodToStateMachineRewriter.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Portable/Lowering/StateMachineRewriter/MethodToStateMachineRewriter.cs
Outdated
Show resolved
Hide resolved
@@ -434,6 +429,50 @@ private void AddVariableCleanup(ArrayBuilder<BoundExpression> cleanup, FieldSymb | |||
} | |||
} | |||
|
|||
#nullable enable | |||
protected BoundBlock GenerateAllHoistedLocalsCleanup() |
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 code looks very different from original implementation of GenerateHoistedLocalsCleanup
. Is it based on some other existing code? Also, isn't there a more direct and robust way to enumerate fields used for hoisting? I mean without going through locals, interpreting their replacements, etc. If not, perhaps we can build the list in advance, when the fields are added. #Closed
@@ -160,7 +160,11 @@ internal void GenerateMoveNextAndDispose(BoundStatement body, SynthesizedImpleme | |||
if (rootFrame.knownStates == null) | |||
{ | |||
// nothing to finalize | |||
F.CloseMethod(F.Return()); | |||
var disposeBody = F.Block( | |||
GenerateAllHoistedLocalsCleanup(), |
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.
Yes, see the diff between commits 1 and 2 for VerifyIL differences.
This change affects all iterator scenarios without try/finally (such as AddVariableCleanup_StringLocal
), whereas the change below affects scenarios with try/finally (such as AddVariableCleanup_NestedStringLocal_InTryFinally
)
@@ -171,6 +175,7 @@ internal void GenerateMoveNextAndDispose(BoundStatement body, SynthesizedImpleme | |||
ImmutableArray.Create<LocalSymbol>(stateLocal), | |||
F.Assignment(F.Local(stateLocal), F.Field(F.This(), stateField)), | |||
EmitFinallyFrame(rootFrame, state), | |||
GenerateAllHoistedLocalsCleanup(), |
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.
Yes. See AddVariableCleanup_NestedStringLocal_InTryFinally
src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncIteratorTests.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncIteratorTests.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncIteratorTests.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncIteratorTests.cs
Outdated
Show resolved
Hide resolved
Done with review pass (commit 5) |
📝 consider renaming or making a local function, consider commenting. This is only for the "master catch" #Closed Refers to: src/Compilers/CSharp/Portable/Lowering/AsyncRewriter/AsyncMethodToStateMachineRewriter.cs:227 in 386a056. [](commit_id = 386a056, deletion_comment = False) |
This reverts commit e170504.
} | ||
|
||
protected BoundStatement GenerateHoistedLocalsCleanup(ImmutableArray<StateMachineFieldSymbol> hoistedLocals) | ||
protected virtual BoundStatement GenerateHoistedLocalsCleanupForExit(ImmutableArray<StateMachineFieldSymbol> rootHoistedLocals) |
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.
Consider removing "HoistedLocals" from the name to completely avoid any confusion that the clean up is limited to the locals in the rootHoistedLocals
parameter.
protected virtual BoundStatement GenerateCleanupForExit(ImmutableArray<StateMachineFieldSymbol> rootHoistedLocals)
``` #Resolved
_fieldsForCleanup = new ArrayBuilder<FieldSymbol>(); | ||
foreach (FieldSymbol fieldForCleanup in nonReusableFieldsForCleanup) | ||
{ | ||
_fieldsForCleanup.Add(fieldForCleanup); |
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.
@@ -428,15 +436,35 @@ internal static bool TryUnwrapBoundStateMachineScope(ref BoundStatement statemen | |||
/// </summary> | |||
private void AddVariableCleanup(ArrayBuilder<BoundExpression> cleanup, FieldSymbol field) | |||
{ | |||
if (field.Type.IsManagedTypeNoUseSiteDiagnostics) | |||
var useSiteInfo = new CompoundUseSiteInfo<AssemblySymbol>(F.Diagnostics, F.Compilation.Assembly); | |||
bool isManaged = field.Type.IsManagedType(ref useSiteInfo); |
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.
} | ||
|
||
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/75666")] | ||
|
||
public void AddVariableCleanup_StringLocal() |
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 shows as an unnecessary empty line between an attribute and the target unit-test. #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.
LGTM (commit 19)
@dotnet/roslyn-compiler for second review. Thanks |
@333fred ptal |
@333fred for a second review. Thanks |
} | ||
finally | ||
{ | ||
System.Console.Write(s); |
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 there a version of this with a throw
in the try
? #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.
No, the only test with a throw
in the try
is AddVariableCleanup_NestedStringLocal_InTryFinally_WithThrow_EarlyIterationExit
.
Fixes #75666
Background
We already have logic to clear unmanaged hoisted locals when we exit scopes. But that logic has a number of limitations:
foreach (var i in coll) { break; }
)So the current design only works for nested locals on normal exit from their scope.
Change
This PR clears the hoisted fields for all the locals (top-level and nested) in the part of the code that handles disposal: in
Dispose
method for iterators and at the end ofMoveNext
for async-iterators.There is no need to add such clearing for async state machines, as it is non-trivial for a user to get a direct reference to those.
The proposed design fixes scenarios 1, 2, 3, and most of 4 (a throw during disposal still results in uncleared state).
This remains "best effort", as we don't want to add the overhead of an additional
try
/finally
to handle that last scenario.Look at the diff between commit 1 (baseline) and commit 2 (code change) to see the expectedOutput and IL changes.