/**
 * Find out in what ways control flow can exit a statement block.
 *
 * Copyright:   Copyright (C) 1999-2023 by The D Language Foundation, All Rights Reserved
 * Authors:     $(LINK2 https://www.digitalmars.com, Walter Bright)
 * License:     $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
 * Source:      $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/blockexit.d, _blockexit.d)
 * Documentation:  https://dlang.org/phobos/dmd_blockexit.html
 * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/blockexit.d
 */

module dmd.blockexit;

import core.stdc.stdio;

import dmd.arraytypes;
import dmd.astenums;
import dmd.canthrow;
import dmd.dclass;
import dmd.declaration;
import dmd.expression;
import dmd.func;
import dmd.globals;
import dmd.id;
import dmd.identifier;
import dmd.location;
import dmd.mtype;
import dmd.statement;
import dmd.tokens;
import dmd.visitor;

/**
 * BE stands for BlockExit.
 *
 * It indicates if a statement does transfer control to another block.
 * A block is a sequence of statements enclosed in { }
 */
enum BE : int
{
    none      = 0,
    fallthru  = 1,
    throw_    = 2,
    return_   = 4,
    goto_     = 8,
    halt      = 0x10,
    break_    = 0x20,
    continue_ = 0x40,
    errthrow  = 0x80,
    any       = (fallthru | throw_ | return_ | goto_ | halt),
}


/*********************************************
 * Determine mask of ways that a statement can exit.
 *
 * Only valid after semantic analysis.
 * Params:
 *   s = statement to check for block exit status
 *   func = function that statement s is in
 *   mustNotThrow = generate an error if it throws
 * Returns:
 *   BE.xxxx
 */
int blockExit(Statement s, FuncDeclaration func, bool mustNotThrow)
{
    extern (C++) final class BlockExit : Visitor
    {
        alias visit = Visitor.visit;
    public:
        FuncDeclaration func;
        bool mustNotThrow;
        int result;

        extern (D) this(FuncDeclaration func, bool mustNotThrow)
        {
            this.func = func;
            this.mustNotThrow = mustNotThrow;
            result = BE.none;
        }

        override void visit(Statement s)
        {
            printf("Statement::blockExit(%p)\n", s);
            printf("%s\n", s.toChars());
            assert(0);
        }

        override void visit(ErrorStatement s)
        {
            result = BE.none;
        }

        override void visit(ExpStatement s)
        {
            result = BE.fallthru;
            if (s.exp)
            {
                if (s.exp.op == EXP.halt)
                {
                    result = BE.halt;
                    return;
                }
                if (AssertExp a = s.exp.isAssertExp())
                {
                    if (a.e1.toBool().hasValue(false)) // if it's an assert(0)
                    {
                        result = BE.halt;
                        return;
                    }
                }
                if (s.exp.type && s.exp.type.toBasetype().isTypeNoreturn())
                    result = BE.halt;

                result |= canThrow(s.exp, func, mustNotThrow);
            }
        }

        override void visit(CompileStatement s)
        {
            assert(global.errors);
            result = BE.fallthru;
        }

        override void visit(CompoundStatement cs)
        {
            //printf("CompoundStatement.blockExit(%p) %d result = x%X\n", cs, cs.statements.length, result);
            result = BE.fallthru;
            Statement slast = null;
            foreach (s; *cs.statements)
            {
                if (s)
                {
                    //printf("result = x%x\n", result);
                    //printf("s: %s\n", s.toChars());
                    if (result & BE.fallthru && slast)
                    {
                        slast = slast.last();
                        if (slast && (slast.isCaseStatement() || slast.isDefaultStatement()) && (s.isCaseStatement() || s.isDefaultStatement()))
                        {
                            // Allow if last case/default was empty
                            CaseStatement sc = slast.isCaseStatement();
                            DefaultStatement sd = slast.isDefaultStatement();
                            auto sl = (sc ? sc.statement : (sd ? sd.statement : null));

                            if (sl && (!sl.hasCode() || sl.isErrorStatement()))
                            {
                            }
                            else if (func.getModule().filetype != FileType.c)
                            {
                                const(char)* gototype = s.isCaseStatement() ? "case" : "default";
                                // @@@DEPRECATED_2.110@@@ https://issues.dlang.org/show_bug.cgi?id=22999
                                // Deprecated in 2.100
                                // Make an error in 2.110
                                if (sl && sl.isCaseStatement())
                                    s.deprecation("switch case fallthrough - use 'goto %s;' if intended", gototype);
                                else
                                    s.error("switch case fallthrough - use 'goto %s;' if intended", gototype);
                            }
                        }
                    }

                    if (!(result & BE.fallthru) && !s.comeFrom())
                    {
                        if (blockExit(s, func, mustNotThrow) != BE.halt && s.hasCode() &&
                            s.loc != Loc.initial) // don't emit warning for generated code
                            s.warning("statement is not reachable");
                    }
                    else
                    {
                        result &= ~BE.fallthru;
                        result |= blockExit(s, func, mustNotThrow);
                    }
                    slast = s;
                }
            }
        }

        override void visit(UnrolledLoopStatement uls)
        {
            result = BE.fallthru;
            foreach (s; *uls.statements)
            {
                if (s)
                {
                    int r = blockExit(s, func, mustNotThrow);
                    result |= r & ~(BE.break_ | BE.continue_ | BE.fallthru);
                    if ((r & (BE.fallthru | BE.continue_ | BE.break_)) == 0)
                        result &= ~BE.fallthru;
                }
            }
        }

        override void visit(ScopeStatement s)
        {
            //printf("ScopeStatement::blockExit(%p)\n", s.statement);
            result = blockExit(s.statement, func, mustNotThrow);
        }

        override void visit(WhileStatement s)
        {
            assert(global.errors);
            result = BE.fallthru;
        }

        override void visit(DoStatement s)
        {
            if (s._body)
            {
                result = blockExit(s._body, func, mustNotThrow);
                if (result == BE.break_)
                {
                    result = BE.fallthru;
                    return;
                }
                if (result & BE.continue_)
                    result |= BE.fallthru;
            }
            else
                result = BE.fallthru;
            if (result & BE.fallthru)
            {
                result |= canThrow(s.condition, func, mustNotThrow);

                if (!(result & BE.break_) && s.condition.toBool().hasValue(true))
                    result &= ~BE.fallthru;
            }
            result &= ~(BE.break_ | BE.continue_);
        }

        override void visit(ForStatement s)
        {
            result = BE.fallthru;
            if (s._init)
            {
                result = blockExit(s._init, func, mustNotThrow);
                if (!(result & BE.fallthru))
                    return;
            }
            if (s.condition)
            {
                result |= canThrow(s.condition, func, mustNotThrow);

                const opt = s.condition.toBool();
                if (opt.hasValue(true))
                    result &= ~BE.fallthru;
                else if (opt.hasValue(false))
                    return;
            }
            else
                result &= ~BE.fallthru; // the body must do the exiting
            if (s._body)
            {
                int r = blockExit(s._body, func, mustNotThrow);
                if (r & (BE.break_ | BE.goto_))
                    result |= BE.fallthru;
                result |= r & ~(BE.fallthru | BE.break_ | BE.continue_);
            }
            if (s.increment)
                result |= canThrow(s.increment, func, mustNotThrow);
        }

        override void visit(ForeachStatement s)
        {
            result = BE.fallthru;
            result |= canThrow(s.aggr, func, mustNotThrow);

            if (s._body)
                result |= blockExit(s._body, func, mustNotThrow) & ~(BE.break_ | BE.continue_);
        }

        override void visit(ForeachRangeStatement s)
        {
            assert(global.errors);
            result = BE.fallthru;
        }

        override void visit(IfStatement s)
        {
            //printf("IfStatement::blockExit(%p)\n", s);
            result = BE.none;
            result |= canThrow(s.condition, func, mustNotThrow);

            const opt = s.condition.toBool();
            if (opt.hasValue(true))
            {
                result |= blockExit(s.ifbody, func, mustNotThrow);
            }
            else if (opt.hasValue(false))
            {
                result |= blockExit(s.elsebody, func, mustNotThrow);
            }
            else
            {
                result |= blockExit(s.ifbody, func, mustNotThrow);
                result |= blockExit(s.elsebody, func, mustNotThrow);
            }
            //printf("IfStatement::blockExit(%p) = x%x\n", s, result);
        }

        override void visit(ConditionalStatement s)
        {
            result = blockExit(s.ifbody, func, mustNotThrow);
            if (s.elsebody)
                result |= blockExit(s.elsebody, func, mustNotThrow);
        }

        override void visit(PragmaStatement s)
        {
            result = BE.fallthru;
        }

        override void visit(StaticAssertStatement s)
        {
            result = BE.fallthru;
        }

        override void visit(SwitchStatement s)
        {
            result = BE.none;
            result |= canThrow(s.condition, func, mustNotThrow);

            if (s._body)
            {
                result |= blockExit(s._body, func, mustNotThrow);
                if (result & BE.break_)
                {
                    result |= BE.fallthru;
                    result &= ~BE.break_;
                }
            }
            else
                result |= BE.fallthru;
        }

        override void visit(CaseStatement s)
        {
            result = blockExit(s.statement, func, mustNotThrow);
        }

        override void visit(DefaultStatement s)
        {
            result = blockExit(s.statement, func, mustNotThrow);
        }

        override void visit(GotoDefaultStatement s)
        {
            result = BE.goto_;
        }

        override void visit(GotoCaseStatement s)
        {
            result = BE.goto_;
        }

        override void visit(SwitchErrorStatement s)
        {
            // Switch errors are non-recoverable
            result = BE.halt;
        }

        override void visit(ReturnStatement s)
        {
            result = BE.return_;
            if (s.exp)
                result |= canThrow(s.exp, func, mustNotThrow);
        }

        override void visit(BreakStatement s)
        {
            //printf("BreakStatement::blockExit(%p) = x%x\n", s, s.ident ? BE.goto_ : BE.break_);
            result = s.ident ? BE.goto_ : BE.break_;
        }

        override void visit(ContinueStatement s)
        {
            result = s.ident ? BE.continue_ | BE.goto_ : BE.continue_;
        }

        override void visit(SynchronizedStatement s)
        {
            result = blockExit(s._body, func, mustNotThrow);
        }

        override void visit(WithStatement s)
        {
            result = BE.none;
            result |= canThrow(s.exp, func, mustNotThrow);
            result |= blockExit(s._body, func, mustNotThrow);
        }

        override void visit(TryCatchStatement s)
        {
            assert(s._body);
            result = blockExit(s._body, func, false);

            int catchresult = 0;
            foreach (c; *s.catches)
            {
                if (c.type == Type.terror)
                    continue;

                int cresult = blockExit(c.handler, func, mustNotThrow);

                /* If we're catching Object, then there is no throwing
                 */
                Identifier id = c.type.toBasetype().isClassHandle().ident;
                if (c.internalCatch && (cresult & BE.fallthru))
                {
                    // https://issues.dlang.org/show_bug.cgi?id=11542
                    // leave blockExit flags of the body
                    cresult &= ~BE.fallthru;
                }
                else if (id == Id.Object || id == Id.Throwable)
                {
                    result &= ~(BE.throw_ | BE.errthrow);
                }
                else if (id == Id.Exception)
                {
                    result &= ~BE.throw_;
                }
                catchresult |= cresult;
            }
            if (mustNotThrow && (result & BE.throw_))
            {
                // now explain why this is nothrow
                blockExit(s._body, func, mustNotThrow);
            }
            result |= catchresult;
        }

        override void visit(TryFinallyStatement s)
        {
            result = BE.fallthru;
            if (s._body)
                result = blockExit(s._body, func, false);

            // check finally body as well, it may throw (bug #4082)
            int finalresult = BE.fallthru;
            if (s.finalbody)
                finalresult = blockExit(s.finalbody, func, false);

            // If either body or finalbody halts
            if (result == BE.halt)
                finalresult = BE.none;
            if (finalresult == BE.halt)
                result = BE.none;

            if (mustNotThrow)
            {
                // now explain why this is nothrow
                if (s._body && (result & BE.throw_))
                    blockExit(s._body, func, mustNotThrow);
                if (s.finalbody && (finalresult & BE.throw_))
                    blockExit(s.finalbody, func, mustNotThrow);
            }

            version (none)
            {
                // https://issues.dlang.org/show_bug.cgi?id=13201
                // Mask to prevent spurious warnings for
                // destructor call, exit of synchronized statement, etc.
                if (result == BE.halt && finalresult != BE.halt && s.finalbody && s.finalbody.hasCode())
                {
                    s.finalbody.warning("statement is not reachable");
                }
            }

            if (!(finalresult & BE.fallthru))
                result &= ~BE.fallthru;
            result |= finalresult & ~BE.fallthru;
        }

        override void visit(ScopeGuardStatement s)
        {
            // At this point, this statement is just an empty placeholder
            result = BE.fallthru;
        }

        override void visit(ThrowStatement s)
        {
            if (s.internalThrow)
            {
                // https://issues.dlang.org/show_bug.cgi?id=8675
                // Allow throwing 'Throwable' object even if mustNotThrow.
                result = BE.fallthru;
                return;
            }

            result = checkThrow(s.loc, s.exp, mustNotThrow);
        }

        override void visit(GotoStatement s)
        {
            //printf("GotoStatement::blockExit(%p)\n", s);
            result = BE.goto_;
        }

        override void visit(LabelStatement s)
        {
            //printf("LabelStatement::blockExit(%p)\n", s);
            result = blockExit(s.statement, func, mustNotThrow);
            if (s.breaks)
                result |= BE.fallthru;
        }

        override void visit(CompoundAsmStatement s)
        {
            // Assume the worst
            result = BE.fallthru | BE.return_ | BE.goto_ | BE.halt;
            if (!(s.stc & STC.nothrow_))
            {
                if (mustNotThrow && !(s.stc & STC.nothrow_))
                    s.error("`asm` statement is assumed to throw - mark it with `nothrow` if it does not");
                else
                    result |= BE.throw_;
            }
        }

        override void visit(ImportStatement s)
        {
            result = BE.fallthru;
        }
    }

    if (!s)
        return BE.fallthru;
    scope BlockExit be = new BlockExit(func, mustNotThrow);
    s.accept(be);
    return be.result;
}

/++
 + Checks whether `throw <exp>` throws an `Exception` or an `Error`
 + and raises an error if this violates `nothrow`.
 +
 + Params:
 +   loc          = location of the `throw`
 +   exp          = expression yielding the throwable
 +   mustNotThrow = inside of a `nothrow` scope?
 +
 + Returns: `BE.[err]throw` depending on the type of `exp`
 +/
BE checkThrow(ref const Loc loc, Expression exp, const bool mustNotThrow)
{
    import dmd.errors : error;

    Type t = exp.type.toBasetype();
    ClassDeclaration cd = t.isClassHandle();
    assert(cd);

    if (cd.isErrorException())
    {
        return BE.errthrow;
    }
    if (mustNotThrow)
        loc.error("`%s` is thrown but not caught", exp.type.toChars());

    return BE.throw_;
}
