/**
 * Perform checks for `nothrow`.
 *
 * Specification: $(LINK2 https://dlang.org/spec/function.html#nothrow-functions, Nothrow Functions)
 *
 * 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/canthrow.d, _canthrow.d)
 * Documentation:  https://dlang.org/phobos/dmd_canthrow.html
 * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/canthrow.d
 */

module dmd.canthrow;

import dmd.aggregate;
import dmd.apply;
import dmd.arraytypes;
import dmd.attrib;
import dmd.astenums;
import dmd.blockexit : BE, checkThrow;
import dmd.declaration;
import dmd.dsymbol;
import dmd.expression;
import dmd.func;
import dmd.globals;
import dmd.init;
import dmd.mtype;
import dmd.root.rootobject;
import dmd.tokens;
import dmd.visitor;

/**
 * Status indicating what kind of throwable might be caused by an expression.
 *
 * This is a subset of `BE` restricted to the values actually used by `canThrow`.
 */
enum CT : BE
{
    /// Never throws an `Exception` or `Throwable`
    none = BE.none,

    /// Might throw an `Exception`
    exception = BE.throw_,

    // Might throw an `Error`
    error = BE.errthrow,
}

/********************************************
 * Returns true if the expression may throw exceptions.
 * If 'mustNotThrow' is true, generate an error if it throws
 */
extern (C++) /* CT */ BE canThrow(Expression e, FuncDeclaration func, bool mustNotThrow)
{
    //printf("Expression::canThrow(%d) %s\n", mustNotThrow, toChars());
    // stop walking if we determine this expression can throw
    extern (C++) final class CanThrow : StoppableVisitor
    {
        alias visit = typeof(super).visit;
        FuncDeclaration func;
        bool mustNotThrow;
        CT result;

    public:
        extern (D) this(FuncDeclaration func, bool mustNotThrow) scope
        {
            this.func = func;
            this.mustNotThrow = mustNotThrow;
        }

        void checkFuncThrows(Expression e, FuncDeclaration f)
        {
            auto tf = f.type.toBasetype().isTypeFunction();
            if (tf && !tf.isnothrow)
            {
                if (mustNotThrow)
                {
                    e.error("%s `%s` is not `nothrow`",
                        f.kind(), f.toPrettyChars());

                    e.checkOverridenDtor(null, f, dd => dd.type.toTypeFunction().isnothrow, "not nothrow");
                }
                result |= CT.exception;
            }
        }

        override void visit(Expression)
        {
        }

        override void visit(DeclarationExp de)
        {
            result |= Dsymbol_canThrow(de.declaration, func, mustNotThrow);
        }

        override void visit(CallExp ce)
        {
            if (ce.inDebugStatement)
                return;

            if (global.errors && !ce.e1.type)
                return; // error recovery

            if (ce.f && ce.arguments.length > 0)
            {
                Type tb = (*ce.arguments)[0].type.toBasetype();
                auto tbNext = tb.nextOf();
                if (tbNext)
                {
                    auto ts = tbNext.baseElemOf().isTypeStruct();
                    if (ts)
                    {
                        auto sd = ts.sym;
                        const id = ce.f.ident;
                        if (sd.postblit && isArrayConstructionOrAssign(id))
                        {
                            checkFuncThrows(ce, sd.postblit);
                            return;
                        }
                    }
                }
            }

            /* If calling a function or delegate that is typed as nothrow,
             * then this expression cannot throw.
             * Note that pure functions can throw.
             */
            if (ce.f && ce.f == func)
                return;
            Type t = ce.e1.type.toBasetype();
            auto tf = t.isTypeFunction();
            if (tf && tf.isnothrow)
                return;
            else
            {
                auto td = t.isTypeDelegate();
                if (td && td.nextOf().isTypeFunction().isnothrow)
                    return;
            }

            if (ce.f)
                checkFuncThrows(ce, ce.f);
            else if (mustNotThrow)
            {
                auto e1 = ce.e1;
                if (auto pe = e1.isPtrExp())   // print 'fp' if e1 is (*fp)
                    e1 = pe.e1;
                ce.error("`%s` is not `nothrow`", e1.toChars());
            }
            result |= CT.exception;
        }

        override void visit(NewExp ne)
        {
            if (ne.member)
            {
                // See if constructor call can throw
                checkFuncThrows(ne, ne.member);
            }
            // regard storage allocation failures as not recoverable
        }

        override void visit(DeleteExp de)
        {
            Type tb = de.e1.type.toBasetype();
            AggregateDeclaration ad = null;
            switch (tb.ty)
            {
            case Tclass:
                ad = tb.isTypeClass().sym;
                break;

            default:
                assert(0);  // error should have been detected by semantic()
            }

            if (ad.dtor)
                checkFuncThrows(de, ad.dtor);
        }

        override void visit(AssignExp ae)
        {
            // blit-init cannot throw
            if (ae.op == EXP.blit)
                return;
            /* Element-wise assignment could invoke postblits.
             */
            Type t;
            if (ae.type.toBasetype().ty == Tsarray)
            {
                if (!ae.e2.isLvalue())
                    return;
                t = ae.type;
            }
            else if (auto se = ae.e1.isSliceExp())
                t = se.e1.type;
            else
                return;

            if (auto ts = t.baseElemOf().isTypeStruct())
                if (auto postblit = ts.sym.postblit)
                    checkFuncThrows(ae, postblit);
        }

        override void visit(ThrowExp te)
        {
            const res = checkThrow(te.loc, te.e1, mustNotThrow);
            assert((res & ~(CT.exception | CT.error)) == 0);
            result |= res;
        }

        override void visit(NewAnonClassExp)
        {
            assert(0); // should have been lowered by semantic()
        }
    }

    scope CanThrow ct = new CanThrow(func, mustNotThrow);
    walkPostorder(e, ct);
    return ct.result;
}

/**************************************
 * Does symbol, when initialized, throw?
 * Mirrors logic in Dsymbol_toElem().
 */
private CT Dsymbol_canThrow(Dsymbol s, FuncDeclaration func, bool mustNotThrow)
{
    CT result;

    int symbolDg(Dsymbol s)
    {
        result |= Dsymbol_canThrow(s, func, mustNotThrow);
        return 0;
    }

    //printf("Dsymbol_toElem() %s\n", s.toChars());
    if (auto vd = s.isVarDeclaration())
    {
        s = s.toAlias();
        if (s != vd)
            return Dsymbol_canThrow(s, func, mustNotThrow);
        if (vd.storage_class & STC.manifest)
        {
        }
        else if (vd.isStatic() || vd.storage_class & (STC.extern_ | STC.gshared))
        {
        }
        else
        {
            if (vd._init)
            {
                if (auto ie = vd._init.isExpInitializer())
                    result |= canThrow(ie.exp, func, mustNotThrow);
            }
            if (vd.needsScopeDtor())
                result |= canThrow(vd.edtor, func, mustNotThrow);
        }
    }
    else if (auto ad = s.isAttribDeclaration())
    {
        ad.include(null).foreachDsymbol(&symbolDg);
    }
    else if (auto tm = s.isTemplateMixin())
    {
        tm.members.foreachDsymbol(&symbolDg);
    }
    else if (auto td = s.isTupleDeclaration())
    {
        td.foreachVar(&symbolDg);
    }
    return result;
}