File: Lowering\AsyncRewriter\AsyncExceptionHandlerRewriter.cs
Web Access
Project: src\src\Compilers\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.csproj (Microsoft.CodeAnalysis.CSharp)
// 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 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);
 
            return loweredStatement;
        }
 
        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);
            }
 
            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)
        {
            // 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));
        }
 
        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));
            }
 
            _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);
 
            var storePending = _F.AssignmentExpression(
                        _F.Local(currentAwaitCatchFrame.pendingCaughtException),
                        _F.Convert(currentAwaitCatchFrame.pendingCaughtException.Type,
                            _F.Local(catchTemp)));
 
            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;
                assignSource = _F.AssignmentExpression(
                                    rewrittenSource,
                                    _F.Convert(
                                        rewrittenSource.Type,
                                        _F.Local(currentAwaitCatchFrame.pendingCaughtException)));
            }
 
            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;
 
            _F.CurrentFunction = node.Symbol;
            _currentAwaitFinallyFrame = new AwaitFinallyFrame();
 
            var result = base.VisitLambda(node);
 
            _F.CurrentFunction = oldContainingSymbol;
            _currentAwaitFinallyFrame = oldAwaitFinallyFrame;
 
            return result;
        }
 
        public override BoundNode VisitLocalFunctionStatement(BoundLocalFunctionStatement node)
        {
            var oldContainingSymbol = _F.CurrentFunction;
            var oldAwaitFinallyFrame = _currentAwaitFinallyFrame;
 
            _F.CurrentFunction = node.Symbol;
            _currentAwaitFinallyFrame = new AwaitFinallyFrame();
 
            var result = base.VisitLocalFunctionStatement(node);
 
            _F.CurrentFunction = oldContainingSymbol;
            _currentAwaitFinallyFrame = oldAwaitFinallyFrame;
 
            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);
            }
        }
    }
}