Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Compilers/CSharp/Portable/Binder/ForEachLoopBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ private BoundForEachStatement BindForEachPartsWorker(BindingDiagnosticBag diagno
var placeholder = new BoundAwaitableValuePlaceholder(expr, builder.MoveNextInfo?.Method.ReturnType ?? CreateErrorType());
awaitInfo = BindAwaitInfo(placeholder, expr, diagnostics, ref hasErrors);

if (!hasErrors && awaitInfo.GetResult?.ReturnType.SpecialType != SpecialType.System_Boolean)
if (!hasErrors && (awaitInfo.GetResult ?? awaitInfo.RuntimeAsyncAwaitMethod)?.ReturnType.SpecialType != SpecialType.System_Boolean)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(awaitInfo.GetResult ?? awaitInfo.RuntimeAsyncAwaitMethod

Can awaitInfo.GetResult ?? awaitInfo.RuntimeAsyncAwaitMethod be null when there's no errors?
I still think it's be a good idea to add a Validate method to BoundAwaitableInfo to codify expectations.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can, when dynamic is involved.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I made the following addition and ran build.cmd -testCompilerOnly -testCoreClr and go no hit:

+                if (!hasErrors)
+                {
+                    Debug.Assert((awaitInfo.GetResult ?? awaitInfo.RuntimeAsyncAwaitMethod) is not null);
+                }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible dynamically-bound awaits do not go through here, but you absolutely can have both methods be null and not have errors.

{
diagnostics.Add(ErrorCode.ERR_BadGetAsyncEnumerator, expr.Location, getEnumeratorMethod.ReturnTypeWithAnnotations, getEnumeratorMethod);
hasErrors = true;
Expand Down
2 changes: 2 additions & 0 deletions src/Compilers/CSharp/Portable/BoundTree/BoundAwaitableInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,7 @@ private partial void Validate()
break;
}
}

Debug.Assert(GetAwaiter is not null || RuntimeAsyncAwaitMethod is not null || IsDynamic || HasErrors);
}
}
5 changes: 4 additions & 1 deletion src/Compilers/CSharp/Portable/CodeGen/CodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,10 @@ private void HandleReturn()
{
_builder.MarkLabel(s_returnLabel);

Debug.Assert(_method.ReturnsVoid == (_returnTemp == null));
Debug.Assert(_method.ReturnsVoid == (_returnTemp == null)
|| (_method.IsAsync
&& _module.Compilation.IsRuntimeAsyncEnabledIn(_method)
&& ((InternalSpecialType)_method.ReturnType.ExtendedSpecialType) is InternalSpecialType.System_Threading_Tasks_Task or InternalSpecialType.System_Threading_Tasks_ValueTask));

if (_emitPdbSequencePoints && !_method.IsIterator && !_method.IsAsync)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ internal sealed class AsyncExceptionHandlerRewriter : BoundTreeRewriterWithStack
private AwaitCatchFrame _currentAwaitCatchFrame;
private AwaitFinallyFrame _currentAwaitFinallyFrame = new AwaitFinallyFrame();
private bool _inCatchWithoutAwaits;
private bool _needsFinalThrow;

private AsyncExceptionHandlerRewriter(
MethodSymbol containingMethod,
Expand Down Expand Up @@ -129,9 +130,45 @@ public static BoundStatement Rewrite(
var rewriter = new AsyncExceptionHandlerRewriter(containingSymbol, containingType, factory, analysis);
var loweredStatement = (BoundStatement)rewriter.Visit(statement);

loweredStatement = rewriter.FinalizeMethodBody(loweredStatement);

return loweredStatement;
}

private BoundStatement FinalizeMethodBody(BoundStatement loweredStatement)
{
if (loweredStatement == null)
{
return null;
}

// When we add a `switch (pendingBranch)` to the end of the try block,
// this can result in a method body that cannot be proven to terminate.
// While we can technically prove it by doing a full data flow analysis,
// this is effectively the halting problem, and the runtime will not do
// this analysis. The resulting IL will be technically invalid, and if it's
// not wrapped in another state machine (a la the compiler async rewriter),
// the runtime will refuse to load it. For runtime async, where we are effectively
// emitting the result of this rewriter directly, we need to ensure that
// we always emit a throw at the end of the try block when the switch is present.
// This ensures that the method can be proven to terminate, and the runtime will
// accept it. This throw will never be reached, and we could potentially do a
// more sophisticated analysis to determine if it is needed by pushing control
// flow analysis through the bound nodes, see https://github.com/dotnet/roslyn/pull/78970.
// This is risky, however, and for now we are taking the conservative approach
// of always emitting the throw.
BoundStatement result = loweredStatement;
if (_needsFinalThrow)
{
result = _F.Block(
loweredStatement,
_F.Throw(_F.Null(_F.SpecialType(SpecialType.System_Object)))
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_F.Throw(_F.Null(_F.SpecialType(SpecialType.System_Object)))

Is this going to work for async void methods? Will there be an explicit return after all user code in the method is exhausted? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope it will, since I expect the automatic implicit return code to not care about whether it's just void or async void, but it's a good test to add.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the automatic implicit return worked here. See AsyncInFinally006_AsyncVoid, newly added.

);
}

return result;
}

public override BoundNode VisitTryStatement(BoundTryStatement node)
{
var tryStatementSyntax = node.Syntax;
Expand Down Expand Up @@ -354,6 +391,7 @@ private BoundStatement UnpendBranches(
cases.Add(caseStatement);
}

_needsFinalThrow = true;
return _F.Switch(_F.Local(pendingBranchVar), cases.ToImmutableAndFree());
}

Expand Down Expand Up @@ -402,22 +440,37 @@ public override BoundNode VisitReturnStatement(BoundReturnStatement node)

private BoundStatement UnpendException(LocalSymbol pendingExceptionLocal)
{
// If this is runtime async, we don't need to create a second local for the exception,
// as the pendingExceptionLocal will not be hoisted to a state machine by a future rewrite.
if (_F.Compilation.IsRuntimeAsyncEnabledIn(_F.CurrentFunction))
{
// pendingExceptionLocal is already an object
// so we can just use it directly
return checkAndThrow(pendingExceptionLocal);
}

// create a temp.
// pendingExceptionLocal will certainly be captured, no need to access it over and over.
LocalSymbol obj = _F.SynthesizedLocal(_F.SpecialType(SpecialType.System_Object));
var objInit = _F.Assignment(_F.Local(obj), _F.Local(pendingExceptionLocal));

// throw pendingExceptionLocal;
BoundStatement rethrow = Rethrow(obj);

return _F.Block(
ImmutableArray.Create<LocalSymbol>(obj),
objInit,
_F.If(
_F.ObjectNotEqual(
_F.Local(obj),
_F.Null(obj.Type)),
rethrow));
checkAndThrow(obj));

BoundStatement checkAndThrow(LocalSymbol obj)
{
BoundStatement rethrow = Rethrow(obj);

BoundStatement checkAndThrow = _F.If(
_F.ObjectNotEqual(
_F.Local(obj),
_F.Null(obj.Type)),
rethrow);
return checkAndThrow;
}
}

private BoundStatement Rethrow(LocalSymbol obj)
Expand Down Expand Up @@ -706,14 +759,24 @@ public override BoundNode VisitLambda(BoundLambda node)
{
var oldContainingSymbol = _F.CurrentFunction;
var oldAwaitFinallyFrame = _currentAwaitFinallyFrame;
var oldNeedsFinalThrow = _needsFinalThrow;

_F.CurrentFunction = node.Symbol;
_currentAwaitFinallyFrame = new AwaitFinallyFrame();
_needsFinalThrow = false;

var result = base.VisitLambda(node);
var result = (BoundLambda)base.VisitLambda(node);
result = result.Update(
result.UnboundLambda,
result.Symbol,
(BoundBlock)FinalizeMethodBody(result.Body),
node.Diagnostics,
node.Binder,
node.Type);

_F.CurrentFunction = oldContainingSymbol;
_currentAwaitFinallyFrame = oldAwaitFinallyFrame;
_needsFinalThrow = oldNeedsFinalThrow;

return result;
}
Expand All @@ -722,14 +785,18 @@ public override BoundNode VisitLocalFunctionStatement(BoundLocalFunctionStatemen
{
var oldContainingSymbol = _F.CurrentFunction;
var oldAwaitFinallyFrame = _currentAwaitFinallyFrame;
var oldNeedsFinalThrow = _needsFinalThrow;

_F.CurrentFunction = node.Symbol;
_currentAwaitFinallyFrame = new AwaitFinallyFrame();
_needsFinalThrow = false;

var result = base.VisitLocalFunctionStatement(node);
var result = (BoundLocalFunctionStatement)base.VisitLocalFunctionStatement(node);
result = result.Update(node.Symbol, (BoundBlock)FinalizeMethodBody(result.Body), (BoundBlock)FinalizeMethodBody(result.ExpressionBody));

_F.CurrentFunction = oldContainingSymbol;
_currentAwaitFinallyFrame = oldAwaitFinallyFrame;
_needsFinalThrow = oldNeedsFinalThrow;

return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,17 @@ public static BoundStatement Rewrite(
return node;
}

// PROTOTYPE: try/finally rewriting
// PROTOTYPE: struct lifting
var rewriter = new RuntimeAsyncRewriter(compilationState.Compilation, new SyntheticBoundNodeFactory(method, node.Syntax, compilationState, diagnostics));
var rewriter = new RuntimeAsyncRewriter(new SyntheticBoundNodeFactory(method, node.Syntax, compilationState, diagnostics));
var result = (BoundStatement)rewriter.Visit(node);
return SpillSequenceSpiller.Rewrite(result, method, compilationState, diagnostics);
}

private readonly CSharpCompilation _compilation;
private readonly SyntheticBoundNodeFactory _factory;
private readonly Dictionary<BoundAwaitableValuePlaceholder, BoundExpression> _placeholderMap;

private RuntimeAsyncRewriter(CSharpCompilation compilation, SyntheticBoundNodeFactory factory)
private RuntimeAsyncRewriter(SyntheticBoundNodeFactory factory)
{
_compilation = compilation;
_factory = factory;
_placeholderMap = [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,14 +223,14 @@ private BoundStatement RewriteForEachEnumerator(
var disposalFinallyBlock = GetDisposalFinallyBlock(forEachSyntax, enumeratorInfo, enumeratorType, boundEnumeratorVar, out var hasAsyncDisposal);
if (isAsync)
{
Debug.Assert(awaitableInfo is { GetResult: { } });
Debug.Assert(awaitableInfo is { GetResult: not null } or { RuntimeAsyncAwaitMethod: not null });

// We need to be sure that when the disposal isn't async we reserve an unused state machine state number for it,
// so that await foreach always produces 2 state machine states: one for MoveNextAsync and the other for DisposeAsync.
// Otherwise, EnC wouldn't be able to map states when the disposal changes from having async dispose to not, or vice versa.
var debugInfo = new BoundAwaitExpressionDebugInfo(s_moveNextAsyncAwaitId, ReservedStateMachineCount: (byte)(hasAsyncDisposal ? 0 : 1));

rewrittenCondition = RewriteAwaitExpression(forEachSyntax, rewrittenCondition, awaitableInfo, awaitableInfo.GetResult.ReturnType, debugInfo, used: true);
rewrittenCondition = RewriteAwaitExpression(forEachSyntax, rewrittenCondition, awaitableInfo, (awaitableInfo.GetResult ?? awaitableInfo.RuntimeAsyncAwaitMethod)!.ReturnType, debugInfo, used: true);
}

BoundStatement whileLoop = RewriteWhileStatement(
Expand Down
18 changes: 14 additions & 4 deletions src/Compilers/CSharp/Portable/Lowering/SpillSequenceSpiller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -704,15 +704,24 @@ public override BoundNode VisitYieldReturnStatement(BoundYieldReturnStatement no
return UpdateStatement(builder, node.Update(expression));
}

#nullable enable
public override BoundNode VisitCatchBlock(BoundCatchBlock node)
{
BoundExpression exceptionSourceOpt = (BoundExpression)this.Visit(node.ExceptionSourceOpt);
BoundExpression? exceptionSourceOpt = (BoundExpression?)this.Visit(node.ExceptionSourceOpt);
var locals = node.Locals;

var exceptionFilterPrologueOpt = node.ExceptionFilterPrologueOpt;
Debug.Assert(exceptionFilterPrologueOpt is null); // it is introduced by this pass
BoundSpillSequenceBuilder builder = null;
if (exceptionFilterPrologueOpt is not null)
{
exceptionFilterPrologueOpt = (BoundStatementList?)VisitStatementList(exceptionFilterPrologueOpt);
}
BoundSpillSequenceBuilder? builder = null;

var exceptionFilterOpt = VisitExpression(ref builder, node.ExceptionFilterOpt);
Debug.Assert(exceptionFilterPrologueOpt is null || builder is null,
"You are exercising SpillSequenceSpiller in a new fashion, causing a spill in an exception filter after LocalRewriting is complete. This is not someting " +
"that this builder supports today, so please update this rewrite to include the statements from exceptionFilterPrologueOpt with the appropriate " +
"syntax node and tracking.");
if (builder is { })
{
Debug.Assert(builder.Value is null);
Expand All @@ -721,9 +730,10 @@ public override BoundNode VisitCatchBlock(BoundCatchBlock node)
}

BoundBlock body = (BoundBlock)this.Visit(node.Body);
TypeSymbol exceptionTypeOpt = this.VisitType(node.ExceptionTypeOpt);
TypeSymbol? exceptionTypeOpt = this.VisitType(node.ExceptionTypeOpt);
return node.Update(locals, exceptionSourceOpt, exceptionTypeOpt, exceptionFilterPrologueOpt, exceptionFilterOpt, body, node.IsSynthesizedAsyncCatchAll);
}
#nullable disable

#if DEBUG
public override BoundNode DefaultVisit(BoundNode node)
Expand Down
Loading