Skip to content

Commit 35ef3fa

Browse files
committed
Use reachability to influence how the async exception handler rewrites try blocks. This is a work in progress
1 parent 5cca81b commit 35ef3fa

28 files changed

+879
-224
lines changed

src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ BoundBlock runAnalysis(BoundBlock block, BindingDiagnosticBag blockDiagnostics)
617617
// It's a bit of code duplication, but refactoring would make things worse.
618618
// However, we don't need to report diagnostics here. They will be reported when analyzing the parent method.
619619
var ignored = DiagnosticBag.GetInstance();
620-
var endIsReachable = ControlFlowPass.Analyze(localSymbol.DeclaringCompilation, localSymbol, block, ignored);
620+
var endIsReachable = ControlFlowPass.Analyze(localSymbol.DeclaringCompilation, localSymbol, block, ignored, out PooledDictionary<BoundNode, bool> asyncTryFinallyEndsReachable);
621621
ignored.Free();
622622
if (endIsReachable)
623623
{
@@ -630,6 +630,12 @@ BoundBlock runAnalysis(BoundBlock block, BindingDiagnosticBag blockDiagnostics)
630630
blockDiagnostics.Add(ErrorCode.ERR_ReturnExpected, localSymbol.GetFirstLocation(), localSymbol);
631631
}
632632
}
633+
634+
if (asyncTryFinallyEndsReachable != null)
635+
{
636+
block = (BoundBlock)FlowAnalysisPass.AsyncTryFinallyEndsReachableRewriter.Rewrite(block, asyncTryFinallyEndsReachable);
637+
asyncTryFinallyEndsReachable.Free();
638+
}
633639
}
634640

635641
return block;
@@ -3220,7 +3226,7 @@ private BoundTryStatement BindTryStatement(TryStatementSyntax node, BindingDiagn
32203226
var tryBlock = BindEmbeddedBlock(node.Block, diagnostics);
32213227
var catchBlocks = BindCatchBlocks(node.Catches, diagnostics);
32223228
var finallyBlockOpt = (node.Finally != null) ? BindEmbeddedBlock(node.Finally.Block, diagnostics) : null;
3223-
return new BoundTryStatement(node, tryBlock, catchBlocks, finallyBlockOpt);
3229+
return new BoundTryStatement(node, tryBlock, catchBlocks, finallyBlockOpt, AsyncTryFinallyEndReachable.Unknown);
32243230
}
32253231

32263232
private ImmutableArray<BoundCatchBlock> BindCatchBlocks(SyntaxList<CatchClauseSyntax> catchClauses, BindingDiagnosticBag diagnostics)

src/Compilers/CSharp/Portable/Binder/ForEachLoopBinder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ private BoundForEachStatement BindForEachPartsWorker(BindingDiagnosticBag diagno
455455
deconstructStep,
456456
awaitInfo,
457457
body,
458+
AsyncTryFinallyEndReachable.Unknown,
458459
this.BreakLabel,
459460
this.ContinueLabel,
460461
hasErrors);
@@ -596,6 +597,7 @@ private BoundForEachStatement BindForEachPartsWorker(BindingDiagnosticBag diagno
596597
deconstructStep,
597598
awaitInfo,
598599
body,
600+
AsyncTryFinallyEndReachable.Unknown,
599601
this.BreakLabel,
600602
this.ContinueLabel,
601603
hasErrors);

src/Compilers/CSharp/Portable/Binder/UsingStatementBinder.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ internal static BoundStatement BindUsingStatementOrDeclarationFromParts(SyntaxNo
164164
// In the future it might be better to have a separate shared type that we add the info to, and have the callers create the appropriate bound nodes from it
165165
if (isUsingDeclaration)
166166
{
167-
return new BoundUsingLocalDeclarations(syntax, patternDisposeInfo, awaitOpt, declarationsOpt, hasErrors);
167+
return new BoundUsingLocalDeclarations(syntax, patternDisposeInfo, awaitOpt, AsyncTryFinallyEndReachable.Unknown, declarationsOpt, hasErrors);
168168
}
169169
else
170170
{
@@ -178,6 +178,7 @@ internal static BoundStatement BindUsingStatementOrDeclarationFromParts(SyntaxNo
178178
boundBody,
179179
awaitOpt,
180180
patternDisposeInfo,
181+
AsyncTryFinallyEndReachable.Unknown,
181182
hasErrors);
182183
}
183184

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace Microsoft.CodeAnalysis.CSharp;
6+
7+
/// <summary>
8+
/// Tracks the reachability of the end of a bound node that either already is a try/finally, or has the potential to become one during lowering.
9+
/// Only nodes that can have an await in the finally block are tracked with this enum, for use in <see cref="AsyncExceptionHandlerRewriter.VisitTryStatement(BoundTryStatement)"/>.
10+
/// Try/finally nodes that are synthesized by the compiler during lowering and do not have an await in the finally block can use <see cref="AsyncTryFinallyEndReachable.Ignored"/>.
11+
/// </summary>
12+
internal enum AsyncTryFinallyEndReachable
13+
{
14+
/// <summary>
15+
/// The reachability of the end of the node has not been determined yet.
16+
/// </summary>
17+
Unknown,
18+
19+
/// <summary>
20+
/// The end of the node is reachable.
21+
/// </summary>
22+
Reachable,
23+
24+
/// <summary>
25+
/// The end of the node is unreachable.
26+
/// </summary>
27+
Unreachable,
28+
29+
/// <summary>
30+
/// The reachability of the end of the node is intentionally not set. This should never be encountered in a try/finally that has an await in the finally block.
31+
/// </summary>
32+
Ignored
33+
}

src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<ValueType Name="CollectionExpressionTypeKind"/>
4141
<ValueType Name="AccessorKind" />
4242
<ValueType Name="SafeContext" />
43+
<ValueType Name="AsyncTryFinallyEndReachable"/>
4344

4445
<AbstractNode Name="BoundInitializer" Base="BoundNode"/>
4546

@@ -1124,6 +1125,7 @@
11241125
<Node Name="BoundUsingLocalDeclarations" Base="BoundMultipleLocalDeclarationsBase">
11251126
<Field Name="PatternDisposeInfoOpt" Type="MethodArgumentInfo?"/>
11261127
<Field Name="AwaitOpt" Type="BoundAwaitableInfo?"/>
1128+
<Field Name="EndIsReachable" Type="AsyncTryFinallyEndReachable" />
11271129
</Node>
11281130

11291131
<!--
@@ -1285,6 +1287,7 @@
12851287
<Field Name="DeconstructionOpt" Type="BoundForEachDeconstructStep?"/>
12861288
<Field Name="AwaitOpt" Type="BoundAwaitableInfo?"/>
12871289
<Field Name="Body" Type="BoundStatement"/>
1290+
<Field Name="EndIsReachable" Type="AsyncTryFinallyEndReachable" />
12881291
</Node>
12891292

12901293
<!-- All the information need to apply a deconstruction at each iteration of a foreach loop involving a deconstruction-declaration. -->
@@ -1301,6 +1304,7 @@
13011304
<Field Name="Body" Type="BoundStatement"/>
13021305
<Field Name="AwaitOpt" Type="BoundAwaitableInfo?"/>
13031306
<Field Name="PatternDisposeInfoOpt" Type="MethodArgumentInfo?"/>
1307+
<Field Name="EndIsReachable" Type="AsyncTryFinallyEndReachable" />
13041308
</Node>
13051309

13061310
<Node Name="BoundFixedStatement" Base="BoundStatement">
@@ -1314,7 +1318,7 @@
13141318
<Field Name="Body" Type="BoundStatement"/>
13151319
</Node>
13161320

1317-
<Node Name="BoundTryStatement" Base="BoundStatement">
1321+
<Node Name="BoundTryStatement" Base="BoundStatement" HasValidate="true">
13181322
<Field Name="TryBlock" Type="BoundBlock"/>
13191323
<Field Name="CatchBlocks" Type="ImmutableArray&lt;BoundCatchBlock&gt;"/>
13201324
<Field Name="FinallyBlockOpt" Type="BoundBlock?"/>
@@ -1354,6 +1358,8 @@
13541358
-->
13551359

13561360
<Field Name="PreferFaultHandler" Type="bool"/>
1361+
1362+
<Field Name="EndIsReachable" Type="AsyncTryFinallyEndReachable" />
13571363
</Node>
13581364

13591365
<Node Name="BoundCatchBlock" Base="BoundNode">
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Diagnostics;
6+
7+
namespace Microsoft.CodeAnalysis.CSharp;
8+
9+
internal partial class BoundTryStatement : BoundStatement
10+
{
11+
private partial void Validate()
12+
{
13+
#if DEBUG
14+
if (EndIsReachable == AsyncTryFinallyEndReachable.Ignored && FinallyBlockOpt != null)
15+
{
16+
Debug.Assert(!AsyncExceptionHandlerRewriter.AwaitsInFinally(FinallyBlockOpt),
17+
"""
18+
There are awaits in this finally block, reachability cannot be ignored! Ensure that valid reachability
19+
is set for this try statement, and ensure that we have async tests for the case where the end of the
20+
try is not reachable and the construct is in a Task<T> or ValueTask<T>-returning method, and runtime
21+
async is enabled for the test case to ensure that we not branching to invalid locations.
22+
""");
23+
}
24+
#endif
25+
}
26+
}

src/Compilers/CSharp/Portable/BoundTree/Constructors.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -613,8 +613,8 @@ public BoundDefaultExpression(SyntaxNode syntax, TypeSymbol type, bool hasErrors
613613

614614
internal partial class BoundTryStatement
615615
{
616-
public BoundTryStatement(SyntaxNode syntax, BoundBlock tryBlock, ImmutableArray<BoundCatchBlock> catchBlocks, BoundBlock? finallyBlockOpt, LabelSymbol? finallyLabelOpt = null)
617-
: this(syntax, tryBlock, catchBlocks, finallyBlockOpt, finallyLabelOpt, preferFaultHandler: false, hasErrors: false)
616+
public BoundTryStatement(SyntaxNode syntax, BoundBlock tryBlock, ImmutableArray<BoundCatchBlock> catchBlocks, BoundBlock? finallyBlockOpt, AsyncTryFinallyEndReachable endIsReachable, LabelSymbol? finallyLabelOpt = null)
617+
: this(syntax, tryBlock, catchBlocks, finallyBlockOpt, finallyLabelOpt, preferFaultHandler: false, endIsReachable: endIsReachable, hasErrors: false)
618618
{
619619
}
620620
}

src/Compilers/CSharp/Portable/BoundTree/UnboundLambda.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -867,7 +867,7 @@ private BoundLambda ReallyBind(NamedTypeSymbol delegateType, bool inExpressionTr
867867

868868
ValidateUnsafeParameters(diagnostics, cacheKey.ParameterTypes);
869869

870-
bool reachableEndpoint = ControlFlowPass.Analyze(compilation, lambdaSymbol, block, diagnostics.DiagnosticBag);
870+
bool reachableEndpoint = ControlFlowPass.Analyze(compilation, lambdaSymbol, block, diagnostics.DiagnosticBag, out PooledDictionary<BoundNode, bool>? asyncTryFinallyEndsReachable);
871871
if (reachableEndpoint)
872872
{
873873
if (Binder.MethodOrLambdaRequiresValue(lambdaSymbol, this.Binder.Compilation))
@@ -881,6 +881,14 @@ private BoundLambda ReallyBind(NamedTypeSymbol delegateType, bool inExpressionTr
881881
}
882882
}
883883

884+
if (asyncTryFinallyEndsReachable != null)
885+
{
886+
// If the lambda is async, we need to ensure that the async try-finally endpoints are reachable.
887+
// This is necessary for correct code generation.
888+
block = (BoundBlock)FlowAnalysisPass.AsyncTryFinallyEndsReachableRewriter.Rewrite(block, asyncTryFinallyEndsReachable);
889+
asyncTryFinallyEndsReachable.Free();
890+
}
891+
884892
if (IsAsync && !ErrorFacts.PreventsSuccessfulDelegateConversion(diagnostics.DiagnosticBag))
885893
{
886894
if (returnType.HasType && // Can be null if "delegateType" is not actually a delegate type.

src/Compilers/CSharp/Portable/CodeGen/CodeGenerator.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,13 @@ private void HandleReturn()
321321
{
322322
_builder.MarkLabel(s_returnLabel);
323323

324-
Debug.Assert(_method.ReturnsVoid == (_returnTemp == null));
324+
Debug.Assert((_method.ReturnsVoid == (_returnTemp == null))
325+
|| (_method.ReturnsVoid
326+
&& _module.Compilation.IsRuntimeAsyncEnabledIn(_method)
327+
&& ((InternalSpecialType)_method.ReturnType.OriginalDefinition.ExtendedSpecialType) is InternalSpecialType.System_Threading_Tasks_Task or InternalSpecialType.System_Threading_Tasks_ValueTask),
328+
"return temp should be null for void methods, or for async Task/ValueTask methods");
325329

330+
// PROTOTYPE: Revisit
326331
if (_emitPdbSequencePoints && !_method.IsIterator && !_method.IsAsync)
327332
{
328333
// In debug mode user could set a breakpoint on the last "}" of the method and

src/Compilers/CSharp/Portable/CodeGen/Optimizer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1676,7 +1676,7 @@ public override BoundNode VisitTryStatement(BoundTryStatement node)
16761676

16771677
EnsureOnlyEvalStack();
16781678

1679-
return node.Update(tryBlock, catchBlocks, finallyBlock, finallyLabelOpt: node.FinallyLabelOpt, node.PreferFaultHandler);
1679+
return node.Update(tryBlock, catchBlocks, finallyBlock, finallyLabelOpt: node.FinallyLabelOpt, node.PreferFaultHandler, node.EndIsReachable);
16801680
}
16811681

16821682
public override BoundNode VisitCatchBlock(BoundCatchBlock node)

0 commit comments

Comments
 (0)