/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Compilers/CSharp/Portable/Lowering/AsyncRewriter/AsyncExceptionHandlerRewriter.cs
1 173 строки
48 KB
AlekseyTs
SyntheticBoundNodeFactory: Remove ability to create arbitrary conversions (#81569)
09 дек 2025, 00:46
Не верифицирован
09 дек 2025, 00:46
8102dd3
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. #nullable disable using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp { /// <summary> /// The purpose of this rewriter is to replace await-containing catch and finally handlers /// with surrogate replacements that keep actual handler code in regular code blocks. /// That allows these constructs to be further lowered at the async lowering pass. /// </summary> internal sealed class AsyncExceptionHandlerRewriter : BoundTreeRewriterWithStackGuardWithoutRecursionOnTheLeftOfBinaryOperator { private readonly SyntheticBoundNodeFactory _F; private readonly AwaitInFinallyAnalysis _analysis; private AwaitCatchFrame _currentAwaitCatchFrame; private AwaitFinallyFrame _currentAwaitFinallyFrame = new AwaitFinallyFrame(); private bool _inCatchWithoutAwaits; private bool _needsFinalThrow; private AsyncExceptionHandlerRewriter( MethodSymbol containingMethod, NamedTypeSymbol containingType, SyntheticBoundNodeFactory factory, AwaitInFinallyAnalysis analysis) { _F = factory; _F.CurrentFunction = containingMethod; Debug.Assert(TypeSymbol.Equals(factory.CurrentType, (containingType ?? containingMethod.ContainingType), TypeCompareKind.ConsiderEverything2)); _analysis = analysis; } /// <summary> /// Lower a block of code by performing local rewritings. /// The goal is to not have exception handlers that contain awaits in them. /// /// 1) Await containing finally blocks: /// The general strategy is to rewrite await containing handlers into synthetic handlers. /// Synthetic handlers are not handlers in IL sense so it is ok to have awaits in them. /// Since synthetic handlers are just blocks, we have to deal with pending exception/branch/return manually /// (this is the hard part of the rewrite). /// /// try{ /// code; /// }finally{ /// handler; /// } /// /// Into ===> /// /// Exception ex = null; /// int pendingBranch = 0; /// /// try{ /// code; // any gotos/returns are rewritten to code that pends the necessary info and goes to finallyLabel /// goto finallyLabel; /// }catch (ex){ // essentially pend the currently active exception /// }; /// /// finallyLabel: /// { /// handler; /// if (ex != null) throw ex; // unpend the exception /// unpend branches/return /// } /// /// 2) Await containing catches: /// try{ /// code; /// }catch (Exception ex){ /// handler; /// throw; /// } /// /// /// Into ===> /// /// Object pendingException; /// int pendingCatch = 0; /// /// try{ /// code; /// }catch (Exception temp){ // essentially pend the currently active exception /// pendingException = temp; /// pendingCatch = 1; /// }; /// /// switch(pendingCatch): /// { /// case 1: /// { /// Exception ex = (Exception)pendingException; /// handler; /// throw pendingException /// } /// } /// </summary> public static BoundStatement Rewrite( MethodSymbol containingSymbol, NamedTypeSymbol containingType, BoundStatement statement, TypeCompilationState compilationState, BindingDiagnosticBag diagnostics) { Debug.Assert(containingSymbol != null); Debug.Assert((object)containingType != null); Debug.Assert(statement != null); Debug.Assert(compilationState != null); Debug.Assert(diagnostics != null); var analysis = new AwaitInFinallyAnalysis(statement); if (!analysis.ContainsAwaitInHandlers()) { return statement; } var factory = new SyntheticBoundNodeFactory(containingSymbol, statement.Syntax, compilationState, diagnostics); 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))) ); } return result; } public override BoundNode VisitTryStatement(BoundTryStatement node) { var tryStatementSyntax = node.Syntax; // If you add a syntax kind to the assertion below, please also ensure // that the scenario has been tested with Edit-and-Continue. Debug.Assert(SyntaxBindingUtilities.BindsToTryStatement(tryStatementSyntax)); var oldTrySyntax = _F.Syntax; _F.Syntax = tryStatementSyntax; var result = visitTryStatement(node, tryStatementSyntax); _F.Syntax = oldTrySyntax; return result; BoundNode visitTryStatement(BoundTryStatement node, SyntaxNode tryStatementSyntax) { BoundStatement finalizedRegion; BoundBlock rewrittenFinally; var finallyContainsAwaits = _analysis.FinallyContainsAwaits(node); if (!finallyContainsAwaits) { finalizedRegion = RewriteFinalizedRegion(node); rewrittenFinally = (BoundBlock)this.Visit(node.FinallyBlockOpt); if (rewrittenFinally == null) { return finalizedRegion; } var asTry = finalizedRegion as BoundTryStatement; if (asTry != null) { // since finalized region is a try we can just attach finally to it Debug.Assert(asTry.FinallyBlockOpt == null); return asTry.Update(asTry.TryBlock, asTry.CatchBlocks, rewrittenFinally, asTry.FinallyLabelOpt, asTry.PreferFaultHandler); } else { // wrap finalizedRegion into a Try with a finally. return _F.Try((BoundBlock)finalizedRegion, ImmutableArray<BoundCatchBlock>.Empty, rewrittenFinally); } } // rewrite finalized region (try and catches) in the current frame var frame = PushFrame(node); finalizedRegion = RewriteFinalizedRegion(node); rewrittenFinally = (BoundBlock)this.VisitBlock(node.FinallyBlockOpt); PopFrame(); var exceptionType = _F.SpecialType(SpecialType.System_Object); var pendingExceptionLocal = new SynthesizedLocal(_F.CurrentFunction, TypeWithAnnotations.Create(exceptionType), SynthesizedLocalKind.TryAwaitPendingException, tryStatementSyntax); var finallyLabel = _F.GenerateLabel("finallyLabel"); var pendingBranchVar = new SynthesizedLocal(_F.CurrentFunction, TypeWithAnnotations.Create(_F.SpecialType(SpecialType.System_Int32)), SynthesizedLocalKind.TryAwaitPendingBranch, tryStatementSyntax); var catchAll = _F.Catch(_F.Local(pendingExceptionLocal), _F.Block()); var catchAndPendException = _F.Try( _F.Block( finalizedRegion, _F.HiddenSequencePoint(), _F.Goto(finallyLabel), PendBranches(frame, pendingBranchVar, finallyLabel)), ImmutableArray.Create(catchAll), finallyLabel: finallyLabel); BoundBlock syntheticFinallyBlock = _F.Block( _F.HiddenSequencePoint(), _F.Label(finallyLabel), rewrittenFinally, _F.HiddenSequencePoint(), UnpendException(pendingExceptionLocal), UnpendBranches( frame, pendingBranchVar)); BoundStatement syntheticFinally = syntheticFinallyBlock; if (_F.CurrentFunction.IsAsync && _F.CurrentFunction.IsIterator) { // We wrap this block so that it can be processed as a finally block by async-iterator rewriting syntheticFinally = _F.ExtractedFinallyBlock(syntheticFinallyBlock); } var locals = ArrayBuilder<LocalSymbol>.GetInstance(); var statements = ArrayBuilder<BoundStatement>.GetInstance(); statements.Add(_F.HiddenSequencePoint()); locals.Add(pendingExceptionLocal); statements.Add(_F.Assignment(_F.Local(pendingExceptionLocal), _F.Default(pendingExceptionLocal.Type))); locals.Add(pendingBranchVar); statements.Add(_F.Assignment(_F.Local(pendingBranchVar), _F.Default(pendingBranchVar.Type))); LocalSymbol returnLocal = frame.returnValue; if (returnLocal != null) { locals.Add(returnLocal); } statements.Add(catchAndPendException); statements.Add(syntheticFinally); var completeTry = _F.Block( locals.ToImmutableAndFree(), statements.ToImmutableAndFree()); return completeTry; } } private BoundBlock PendBranches( AwaitFinallyFrame frame, LocalSymbol pendingBranchVar, LabelSymbol finallyLabel) { var bodyStatements = ArrayBuilder<BoundStatement>.GetInstance(); // handle proxy labels if have any var proxiedLabels = frame.proxiedLabels; var proxyLabels = frame.proxyLabels; // skip 0 - it means we took no explicit branches int i = 1; if (proxiedLabels != null) { for (int cnt = proxiedLabels.Count; i <= cnt; i++) { var proxied = proxiedLabels[i - 1]; var proxy = proxyLabels[proxied]; PendBranch(bodyStatements, proxy, i, pendingBranchVar, finallyLabel); } } var returnProxy = frame.returnProxyLabel; if (returnProxy != null) { PendBranch(bodyStatements, returnProxy, i, pendingBranchVar, finallyLabel); } return _F.Block(bodyStatements.ToImmutableAndFree()); } private void PendBranch( ArrayBuilder<BoundStatement> bodyStatements, LabelSymbol proxy, int i, LocalSymbol pendingBranchVar, LabelSymbol finallyLabel) { // branch lands here bodyStatements.Add(_F.Label(proxy)); // pend the branch bodyStatements.Add(_F.Assignment(_F.Local(pendingBranchVar), _F.Literal(i))); // skip other proxies bodyStatements.Add(_F.Goto(finallyLabel)); } private BoundStatement UnpendBranches( AwaitFinallyFrame frame, SynthesizedLocal pendingBranchVar) { var parent = frame.ParentOpt; // handle proxy labels if have any var proxiedLabels = frame.proxiedLabels; // skip 0 - it means we took no explicit branches int i = 1; var cases = ArrayBuilder<SyntheticBoundNodeFactory.SyntheticSwitchSection>.GetInstance(); if (proxiedLabels != null) { for (int cnt = proxiedLabels.Count; i <= cnt; i++) { var target = proxiedLabels[i - 1]; var parentProxy = parent.ProxyLabelIfNeeded(target); var caseStatement = _F.SwitchSection(i, _F.Goto(parentProxy)); cases.Add(caseStatement); } } if (frame.returnProxyLabel != null) { BoundLocal pendingValue = null; if (frame.returnValue != null) { pendingValue = _F.Local(frame.returnValue); } SynthesizedLocal returnValue; BoundStatement unpendReturn; var returnLabel = parent.ProxyReturnIfNeeded(_F.CurrentFunction, pendingValue, out returnValue); if (returnLabel == null) { unpendReturn = new BoundReturnStatement(_F.Syntax, RefKind.None, pendingValue, @checked: false); } else { if (pendingValue == null) { unpendReturn = _F.Goto(returnLabel); } else { unpendReturn = _F.Block( _F.Assignment( _F.Local(returnValue), pendingValue), _F.Goto(returnLabel)); } } var caseStatement = _F.SwitchSection(i, unpendReturn); cases.Add(caseStatement); } _needsFinalThrow = true; return _F.Switch(_F.Local(pendingBranchVar), cases.ToImmutableAndFree()); } public override BoundNode VisitGotoStatement(BoundGotoStatement node) { BoundExpression caseExpressionOpt = (BoundExpression)this.Visit(node.CaseExpressionOpt); BoundLabel labelExpressionOpt = (BoundLabel)this.Visit(node.LabelExpressionOpt); var proxyLabel = _currentAwaitFinallyFrame.ProxyLabelIfNeeded(node.Label); return node.Update(proxyLabel, caseExpressionOpt, labelExpressionOpt); } public override BoundNode VisitConditionalGoto(BoundConditionalGoto node) { Debug.Assert(node.Label == _currentAwaitFinallyFrame.ProxyLabelIfNeeded(node.Label), "conditional leave?"); return base.VisitConditionalGoto(node); } public override BoundNode VisitReturnStatement(BoundReturnStatement node) { SynthesizedLocal returnValue; var returnLabel = _currentAwaitFinallyFrame.ProxyReturnIfNeeded( _F.CurrentFunction, node.ExpressionOpt, out returnValue); if (returnLabel == null) { return base.VisitReturnStatement(node); } var returnExpr = (BoundExpression)(this.Visit(node.ExpressionOpt)); if (returnExpr != null) { return _F.Block( _F.Assignment( _F.Local(returnValue), returnExpr), _F.Goto( returnLabel)); } else { return _F.Goto(returnLabel); } } 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; return _F.Block( ImmutableArray.Create<LocalSymbol>(obj), objInit, 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) { // conservative rethrow BoundStatement rethrow = _F.Throw(_F.Local(obj)); var exceptionDispatchInfoCapture = _F.WellKnownMethod(WellKnownMember.System_Runtime_ExceptionServices_ExceptionDispatchInfo__Capture, isOptional: true); var exceptionDispatchInfoThrow = _F.WellKnownMethod(WellKnownMember.System_Runtime_ExceptionServices_ExceptionDispatchInfo__Throw, isOptional: true); // if these helpers are available, we can rethrow with original stack info // as long as it derives from Exception if (exceptionDispatchInfoCapture != null && exceptionDispatchInfoThrow != null) { var ex = _F.SynthesizedLocal(_F.WellKnownType(WellKnownType.System_Exception)); var assignment = _F.Assignment( _F.Local(ex), _F.As(_F.Local(obj), ex.Type)); // better rethrow rethrow = _F.Block( ImmutableArray.Create(ex), assignment, _F.If(_F.ObjectEqual(_F.Local(ex), _F.Null(ex.Type)), rethrow), // ExceptionDispatchInfo.Capture(pendingExceptionLocal).Throw(); _F.ExpressionStatement( _F.Call( _F.StaticCall( exceptionDispatchInfoCapture.ContainingType, exceptionDispatchInfoCapture, _F.Local(ex)), exceptionDispatchInfoThrow))); } return rethrow; } /// <summary> /// Rewrites Try/Catch part of the Try/Catch/Finally /// </summary> private BoundStatement RewriteFinalizedRegion(BoundTryStatement node) { var rewrittenTry = (BoundBlock)this.VisitBlock(node.TryBlock); var catches = node.CatchBlocks; if (catches.IsDefaultOrEmpty) { return rewrittenTry; } var origAwaitCatchFrame = _currentAwaitCatchFrame; _currentAwaitCatchFrame = null; var rewrittenCatches = node.CatchBlocks.SelectAsArray(static (catchBlock, arg) => { var (@this, origAwaitCatchFrame) = arg; return (BoundCatchBlock)@this.VisitCatchBlock(catchBlock, parentAwaitCatchFrame: origAwaitCatchFrame); }, (this, origAwaitCatchFrame)); BoundStatement tryWithCatches = _F.Try(rewrittenTry, rewrittenCatches); var currentAwaitCatchFrame = _currentAwaitCatchFrame; if (currentAwaitCatchFrame != null) { var handledLabel = _F.GenerateLabel("handled"); var handlersList = currentAwaitCatchFrame.handlers; var handlers = ArrayBuilder<SyntheticBoundNodeFactory.SyntheticSwitchSection>.GetInstance(handlersList.Count); for (int i = 0, l = handlersList.Count; i < l; i++) { handlers.Add(_F.SwitchSection( i + 1, _F.Block( handlersList[i], _F.Goto(handledLabel)))); } tryWithCatches = _F.Block( ImmutableArray.Create<LocalSymbol>( currentAwaitCatchFrame.pendingCaughtException, currentAwaitCatchFrame.pendingCatch). AddRange(currentAwaitCatchFrame.GetHoistedLocals()), _F.HiddenSequencePoint(), _F.Assignment( _F.Local(currentAwaitCatchFrame.pendingCatch), _F.Default(currentAwaitCatchFrame.pendingCatch.Type)), tryWithCatches, _F.HiddenSequencePoint(), _F.Switch( _F.Local(currentAwaitCatchFrame.pendingCatch), handlers.ToImmutableAndFree()), _F.HiddenSequencePoint(), _F.Label(handledLabel)); // It's possible that all catches end in rethrows, and the method ends after them. In such scenarios, // after the above switch will be "reachable", but have no statements to execute. In practice such code // in unreachable, but this is the halting problem. To ensure we have valid IL, we append a final throw // to the method, and if further basic block optimization determines that it's unreachable, then it'll be // trimmed. _needsFinalThrow = true; } _currentAwaitCatchFrame = origAwaitCatchFrame; return tryWithCatches; } public override BoundNode VisitCatchBlock(BoundCatchBlock node) { throw ExceptionUtilities.Unreachable(); } private BoundNode VisitCatchBlock(BoundCatchBlock node, AwaitCatchFrame parentAwaitCatchFrame) { if (!_analysis.CatchContainsAwait(node)) { var origCurrentAwaitCatchFrame = _currentAwaitCatchFrame; _currentAwaitCatchFrame = parentAwaitCatchFrame; var origInCatchWithoutAwaits = _inCatchWithoutAwaits; _inCatchWithoutAwaits = true; var result = base.VisitCatchBlock(node); _currentAwaitCatchFrame = origCurrentAwaitCatchFrame; _inCatchWithoutAwaits = origInCatchWithoutAwaits; return result; } // We cannot get here from a catch without awaits. Debug.Assert(!_inCatchWithoutAwaits); var currentAwaitCatchFrame = _currentAwaitCatchFrame; if (currentAwaitCatchFrame == null) { Debug.Assert(node.Syntax.IsKind(SyntaxKind.CatchClause)); var tryStatementSyntax = (TryStatementSyntax)node.Syntax.Parent; currentAwaitCatchFrame = _currentAwaitCatchFrame = new AwaitCatchFrame(_F, tryStatementSyntax, parentAwaitCatchFrame); } var catchType = node.ExceptionTypeOpt ?? _F.SpecialType(SpecialType.System_Object); var catchTemp = _F.SynthesizedLocal(catchType); BoundLocal carchTempRef = _F.Local(catchTemp); TypeSymbol pendingCaughtExceptionType = currentAwaitCatchFrame.pendingCaughtException.Type; Debug.Assert(pendingCaughtExceptionType.IsObjectType()); Conversion c = _F.ClassifyEmitConversion(carchTempRef, pendingCaughtExceptionType); Debug.Assert(c.IsImplicit); Debug.Assert(c.IsReference || c.IsIdentity); var storePending = _F.AssignmentExpression( _F.Local(currentAwaitCatchFrame.pendingCaughtException), _F.Convert(pendingCaughtExceptionType, carchTempRef, c)); var setPendingCatchNum = _F.Assignment( _F.Local(currentAwaitCatchFrame.pendingCatch), _F.Literal(currentAwaitCatchFrame.handlers.Count + 1)); // catch (ExType exTemp) // { // pendingCaughtException = exTemp; // catchNo = X; // } BoundCatchBlock catchAndPend; ImmutableArray<LocalSymbol> handlerLocals; var filterPrologueOpt = node.ExceptionFilterPrologueOpt; var filterOpt = node.ExceptionFilterOpt; if (filterOpt == null) { Debug.Assert(filterPrologueOpt is null); // store pending exception // as the first statement in a catch catchAndPend = node.Update( ImmutableArray.Create(catchTemp), _F.Local(catchTemp), catchType, exceptionFilterPrologueOpt: filterPrologueOpt, exceptionFilterOpt: null, body: _F.Block( _F.HiddenSequencePoint(), _F.ExpressionStatement(storePending), setPendingCatchNum), isSynthesizedAsyncCatchAll: node.IsSynthesizedAsyncCatchAll); // catch locals live on the synthetic catch handler block handlerLocals = node.Locals; } else { handlerLocals = ImmutableArray<LocalSymbol>.Empty; // catch locals move up into hoisted locals // since we might need to access them from both the filter and the catch foreach (var local in node.Locals) { currentAwaitCatchFrame.HoistLocal(local, _F); } // store pending exception // as the first expression in a filter prologue var rewrittenPrologue = (BoundStatementList)this.Visit(filterPrologueOpt); var prologueBuilder = ArrayBuilder<BoundStatement>.GetInstance(); var sourceOpt = node.ExceptionSourceOpt; prologueBuilder.Add(_F.ExpressionStatement(storePending)); if (sourceOpt is not null) { prologueBuilder.Add(_F.ExpressionStatement(AssignCatchSource((BoundExpression)this.Visit(sourceOpt), currentAwaitCatchFrame))); } if (rewrittenPrologue != null) { prologueBuilder.Add(rewrittenPrologue); } var newPrologue = _F.StatementList(prologueBuilder.ToImmutableAndFree()); var rewrittenFilter = (BoundExpression)this.Visit(filterOpt); catchAndPend = node.Update( ImmutableArray.Create(catchTemp), _F.Local(catchTemp), catchType, exceptionFilterPrologueOpt: newPrologue, exceptionFilterOpt: rewrittenFilter, body: _F.Block( _F.HiddenSequencePoint(), setPendingCatchNum), isSynthesizedAsyncCatchAll: node.IsSynthesizedAsyncCatchAll); } var handlerStatements = ArrayBuilder<BoundStatement>.GetInstance(); handlerStatements.Add(_F.HiddenSequencePoint()); if (filterOpt == null) { var sourceOpt = node.ExceptionSourceOpt; if (sourceOpt != null) { BoundExpression assignSource = AssignCatchSource((BoundExpression)this.Visit(sourceOpt), currentAwaitCatchFrame); handlerStatements.Add(_F.ExpressionStatement(assignSource)); } } handlerStatements.Add((BoundStatement)this.Visit(node.Body)); var handler = _F.Block( handlerLocals, handlerStatements.ToImmutableAndFree() ); currentAwaitCatchFrame.handlers.Add(handler); return catchAndPend; } private BoundExpression AssignCatchSource(BoundExpression rewrittenSource, AwaitCatchFrame currentAwaitCatchFrame) { BoundExpression assignSource = null; if (rewrittenSource != null) { // exceptionSource = (exceptionSourceType)pendingCaughtException; BoundLocal pendingExceptionRef = _F.Local(currentAwaitCatchFrame.pendingCaughtException); TypeSymbol rewrittenSourceType = rewrittenSource.Type; Debug.Assert(pendingExceptionRef.Type.IsObjectType()); Conversion c = _F.ClassifyEmitConversion(pendingExceptionRef, rewrittenSourceType); Debug.Assert(c.IsReference || c.IsIdentity); assignSource = _F.AssignmentExpression( rewrittenSource, _F.Convert( rewrittenSourceType, pendingExceptionRef, c)); } return assignSource; } public override BoundNode VisitLocal(BoundLocal node) { var catchFrame = _currentAwaitCatchFrame; LocalSymbol hoistedLocal; if (catchFrame == null || !catchFrame.TryGetHoistedLocal(node.LocalSymbol, out hoistedLocal)) { return base.VisitLocal(node); } return node.Update(hoistedLocal, node.ConstantValueOpt, hoistedLocal.Type); } public override BoundNode VisitThrowStatement(BoundThrowStatement node) { // If we are in a catch without awaits, `_currentAwaitCatchFrame` is the nearest ancestor catch frame with awaits // and `_inCatchWithoutAwaits` is `true`. if (node.ExpressionOpt != null || _currentAwaitCatchFrame == null || _inCatchWithoutAwaits) { return base.VisitThrowStatement(node); } return Rethrow(_currentAwaitCatchFrame.pendingCaughtException); } 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 = (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; } public override BoundNode VisitLocalFunctionStatement(BoundLocalFunctionStatement node) { var oldContainingSymbol = _F.CurrentFunction; var oldAwaitFinallyFrame = _currentAwaitFinallyFrame; var oldNeedsFinalThrow = _needsFinalThrow; _F.CurrentFunction = node.Symbol; _currentAwaitFinallyFrame = new AwaitFinallyFrame(); _needsFinalThrow = false; 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; } private AwaitFinallyFrame PushFrame(BoundTryStatement statement) { var newFrame = new AwaitFinallyFrame(_currentAwaitFinallyFrame, _analysis.Labels(statement), statement.Syntax); _currentAwaitFinallyFrame = newFrame; return newFrame; } private void PopFrame() { var result = _currentAwaitFinallyFrame; _currentAwaitFinallyFrame = result.ParentOpt; } /// <summary> /// Analyzes method body for try blocks with awaits in finally blocks /// Also collects labels that such blocks contain. /// </summary> private sealed class AwaitInFinallyAnalysis : LabelCollector { // all try blocks with yields in them and complete set of labels inside those try blocks // NOTE: non-yielding try blocks are transparently ignored - i.e. their labels are included // in the label set of the nearest yielding-try parent private Dictionary<BoundTryStatement, HashSet<LabelSymbol>> _labelsInInterestingTry; private HashSet<BoundCatchBlock> _awaitContainingCatches; // transient accumulators. private bool _seenAwait; public AwaitInFinallyAnalysis(BoundStatement body) { _seenAwait = false; this.Visit(body); } /// <summary> /// Returns true if a finally of the given try contains awaits /// </summary> public bool FinallyContainsAwaits(BoundTryStatement statement) { return _labelsInInterestingTry != null && _labelsInInterestingTry.ContainsKey(statement); } /// <summary> /// Returns true if a catch contains awaits /// </summary> internal bool CatchContainsAwait(BoundCatchBlock node) { return _awaitContainingCatches != null && _awaitContainingCatches.Contains(node); } /// <summary> /// Returns true if body contains await in a finally block. /// </summary> public bool ContainsAwaitInHandlers() { return _labelsInInterestingTry != null || _awaitContainingCatches != null; } /// <summary> /// Labels reachable from within this frame without invoking its finally. /// null if there are no such labels. /// </summary> internal HashSet<LabelSymbol> Labels(BoundTryStatement statement) { return _labelsInInterestingTry[statement]; } public override BoundNode VisitTryStatement(BoundTryStatement node) { var origLabels = this.currentLabels; this.currentLabels = null; Visit(node.TryBlock); VisitList(node.CatchBlocks); var origSeenAwait = _seenAwait; _seenAwait = false; Visit(node.FinallyBlockOpt); if (_seenAwait) { // this try has awaits in the finally ! var labelsInInterestingTry = _labelsInInterestingTry; if (labelsInInterestingTry == null) { _labelsInInterestingTry = labelsInInterestingTry = new Dictionary<BoundTryStatement, HashSet<LabelSymbol>>(); } labelsInInterestingTry.Add(node, currentLabels); currentLabels = origLabels; } else { // this is a boring try without awaits in finally // currentLabels = currentLabels U origLabels ; if (currentLabels == null) { currentLabels = origLabels; } else if (origLabels != null) { currentLabels.UnionWith(origLabels); } } _seenAwait = _seenAwait | origSeenAwait; return null; } public override BoundNode VisitCatchBlock(BoundCatchBlock node) { var origSeenAwait = _seenAwait; _seenAwait = false; var result = base.VisitCatchBlock(node); if (_seenAwait) { var awaitContainingCatches = _awaitContainingCatches; if (awaitContainingCatches == null) { _awaitContainingCatches = awaitContainingCatches = new HashSet<BoundCatchBlock>(); } _awaitContainingCatches.Add(node); } _seenAwait |= origSeenAwait; return result; } public override BoundNode VisitAwaitExpression(BoundAwaitExpression node) { _seenAwait = true; return base.VisitAwaitExpression(node); } public override BoundNode VisitLambda(BoundLambda node) { var origLabels = this.currentLabels; var origSeenAwait = _seenAwait; this.currentLabels = null; _seenAwait = false; base.VisitLambda(node); this.currentLabels = origLabels; _seenAwait = origSeenAwait; return null; } public override BoundNode VisitLocalFunctionStatement(BoundLocalFunctionStatement node) { var origLabels = this.currentLabels; var origSeenAwait = _seenAwait; this.currentLabels = null; _seenAwait = false; base.VisitLocalFunctionStatement(node); this.currentLabels = origLabels; _seenAwait = origSeenAwait; return null; } } // storage of various information about a given finally frame private sealed class AwaitFinallyFrame { // Enclosing frame. Root frame does not have parent. public readonly AwaitFinallyFrame ParentOpt; // labels within this frame (branching to these labels does not go through finally). public readonly HashSet<LabelSymbol> LabelsOpt; // the try or using-await statement the frame is associated with private readonly SyntaxNode _syntaxOpt; // proxy labels for branches leaving the frame. // we build this on demand once we encounter leaving branches. // subsequent leaves to an already proxied label redirected to the proxy. // At the proxy label we will execute finally and forward the control flow // to the actual destination. (which could be proxied again in the parent) public Dictionary<LabelSymbol, LabelSymbol> proxyLabels; public List<LabelSymbol> proxiedLabels; public GeneratedLabelSymbol returnProxyLabel; public SynthesizedLocal returnValue; public AwaitFinallyFrame() { // root frame } public AwaitFinallyFrame(AwaitFinallyFrame parent, HashSet<LabelSymbol> labelsOpt, SyntaxNode syntax) { Debug.Assert(parent != null); Debug.Assert(syntax != null); Debug.Assert(SyntaxBindingUtilities.BindsToTryStatement(syntax)); this.ParentOpt = parent; this.LabelsOpt = labelsOpt; _syntaxOpt = syntax; } public bool IsRoot() { return this.ParentOpt == null; } // returns a proxy for a label if branch must be hijacked to run finally // otherwise returns same label back public LabelSymbol ProxyLabelIfNeeded(LabelSymbol label) { // no need to proxy a label in the current frame or when we are at the root if (this.IsRoot() || (LabelsOpt != null && LabelsOpt.Contains(label))) { return label; } var proxyLabels = this.proxyLabels; var proxiedLabels = this.proxiedLabels; if (proxyLabels == null) { this.proxyLabels = proxyLabels = new Dictionary<LabelSymbol, LabelSymbol>(); this.proxiedLabels = proxiedLabels = new List<LabelSymbol>(); } LabelSymbol proxy; if (!proxyLabels.TryGetValue(label, out proxy)) { proxy = new GeneratedLabelSymbol("proxy" + label.Name); proxyLabels.Add(label, proxy); proxiedLabels.Add(label); } return proxy; } public LabelSymbol ProxyReturnIfNeeded( MethodSymbol containingMethod, BoundExpression valueOpt, out SynthesizedLocal returnValue) { returnValue = null; // no need to proxy returns at the root if (this.IsRoot()) { return null; } var returnProxy = this.returnProxyLabel; if (returnProxy == null) { this.returnProxyLabel = returnProxy = new GeneratedLabelSymbol("returnProxy"); } if (valueOpt != null) { returnValue = this.returnValue; if (returnValue == null) { Debug.Assert(_syntaxOpt != null); this.returnValue = returnValue = new SynthesizedLocal(containingMethod, TypeWithAnnotations.Create(valueOpt.Type), SynthesizedLocalKind.AsyncMethodReturnValue, _syntaxOpt); } } return returnProxy; } } private sealed class AwaitCatchFrame { // object, stores the original caught exception // used to initialize the exception source inside the handler // also used in rethrow statements public readonly SynthesizedLocal pendingCaughtException; // int, stores the number of pending catch // 0 - means no catches are pending. public readonly SynthesizedLocal pendingCatch; // synthetic handlers produced by catch rewrite. // they will become switch sections when pending exception is dispatched. public readonly List<BoundBlock> handlers; private readonly AwaitCatchFrame _parentOpt; // when catch local must be used from a filter // we need to "hoist" it up to ensure that both the filter // and the catch access the same variable. // NOTE: it must be the same variable, not just same value. // The difference would be observable if filter mutates the variable // or/and if a variable gets lifted into a closure. private readonly Dictionary<LocalSymbol, LocalSymbol> _hoistedLocals; private readonly List<LocalSymbol> _orderedHoistedLocals; public AwaitCatchFrame(SyntheticBoundNodeFactory F, TryStatementSyntax tryStatementSyntax, AwaitCatchFrame parentOpt) { this.pendingCaughtException = new SynthesizedLocal(F.CurrentFunction, TypeWithAnnotations.Create(F.SpecialType(SpecialType.System_Object)), SynthesizedLocalKind.TryAwaitPendingCaughtException, tryStatementSyntax); this.pendingCatch = new SynthesizedLocal(F.CurrentFunction, TypeWithAnnotations.Create(F.SpecialType(SpecialType.System_Int32)), SynthesizedLocalKind.TryAwaitPendingCatch, tryStatementSyntax); this.handlers = new List<BoundBlock>(); this._parentOpt = parentOpt; _hoistedLocals = new Dictionary<LocalSymbol, LocalSymbol>(); _orderedHoistedLocals = new List<LocalSymbol>(); } public void HoistLocal(LocalSymbol local, SyntheticBoundNodeFactory F) { if (!_hoistedLocals.Keys.Any(l => l.Name == local.Name && TypeSymbol.Equals(l.Type, local.Type, TypeCompareKind.ConsiderEverything2))) { _hoistedLocals.Add(local, local); _orderedHoistedLocals.Add(local); return; } // code uses "await" in two sibling catches with exception filters // locals with same names and types may cause problems if they are lifted // and become fields with identical signatures. // To avoid such problems we will mangle the name of the second local. // This will only affect debugging of this extremely rare case. Debug.Assert(pendingCatch.SyntaxOpt.IsKind(SyntaxKind.TryStatement)); var newLocal = F.SynthesizedLocal(local.Type, pendingCatch.SyntaxOpt, kind: SynthesizedLocalKind.ExceptionFilterAwaitHoistedExceptionLocal); _hoistedLocals.Add(local, newLocal); _orderedHoistedLocals.Add(newLocal); } public IEnumerable<LocalSymbol> GetHoistedLocals() { return _orderedHoistedLocals; } public bool TryGetHoistedLocal(LocalSymbol originalLocal, out LocalSymbol hoistedLocal) { return _hoistedLocals.TryGetValue(originalLocal, out hoistedLocal) || (_parentOpt?.TryGetHoistedLocal(originalLocal, out hoistedLocal) == true); } } } }