/**
 * Lazily evaluate static conditions for `static if`, `static assert` and template constraints.
 *
 * Copyright:   Copyright (C) 1999-2024 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/staticcond.d, _staticcond.d)
 * Documentation:  https://dlang.org/phobos/dmd_staticcond.html
 * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/staticcond.d
 */

module dmd.staticcond;

import dmd.arraytypes;
import dmd.dinterpret;
import dmd.dmodule;
import dmd.dscope;
import dmd.dsymbol;
import dmd.errors;
import dmd.expression;
import dmd.expressionsem;
import dmd.globals;
import dmd.identifier;
import dmd.mtype;
import dmd.optimize;
import dmd.root.array;
import dmd.common.outbuffer;
import dmd.tokens;



/********************************************
 * Semantically analyze and then evaluate a static condition at compile time.
 * This is special because short circuit operators &&, || and ?: at the top
 * level are not semantically analyzed if the result of the expression is not
 * necessary.
 * Params:
 *      sc  = instantiating scope
 *      original = original expression, for error messages
 *      e =  resulting expression
 *      errors = set to `true` if errors occurred
 *      negatives = array to store negative clauses
 * Returns:
 *      true if evaluates to true
 */
bool evalStaticCondition(Scope* sc, Expression original, Expression e, out bool errors, Expressions* negatives = null)
{
    if (negatives)
        negatives.setDim(0);

    bool impl(Expression e)
    {
        if (e.isNotExp())
        {
            NotExp ne = cast(NotExp)e;
            return !impl(ne.e1);
        }

        if (e.op == EXP.andAnd || e.op == EXP.orOr)
        {
            LogicalExp aae = cast(LogicalExp)e;
            bool result = impl(aae.e1);
            if (errors)
                return false;
            if (e.op == EXP.andAnd)
            {
                if (!result)
                    return false;
            }
            else
            {
                if (result)
                    return true;
            }
            result = impl(aae.e2);
            return !errors && result;
        }

        if (e.op == EXP.question)
        {
            CondExp ce = cast(CondExp)e;
            bool result = impl(ce.econd);
            if (errors)
                return false;
            Expression leg = result ? ce.e1 : ce.e2;
            result = impl(leg);
            return !errors && result;
        }

        Expression before = e;
        const uint nerrors = global.errors;

        sc = sc.startCTFE();
        sc.flags |= SCOPE.condition;

        e = e.expressionSemantic(sc);
        e = resolveProperties(sc, e);
        e = e.toBoolean(sc);

        sc = sc.endCTFE();
        e = e.optimize(WANTvalue);

        if (nerrors != global.errors ||
            e.isErrorExp() ||
            e.type.toBasetype() == Type.terror)
        {
            errors = true;
            return false;
        }

        e = e.ctfeInterpret();

        const opt = e.toBool();
        if (opt.isEmpty())
        {
            if (!e.type.isTypeError())
                error(e.loc, "expression `%s` is not constant", e.toChars());
            errors = true;
            return false;
        }

        if (negatives && !opt.get())
            negatives.push(before);
        return opt.get();
    }
    return impl(e);
}

/********************************************
 * Format a static condition as a tree-like structure, marking failed and
 * bypassed expressions.
 * Params:
 *      original = original expression
 *      instantiated = instantiated expression
 *      negatives = array with negative clauses from `instantiated` expression
 *      full = controls whether it shows the full output or only failed parts
 *      itemCount = returns the number of written clauses
 * Returns:
 *      formatted string or `null` if the expressions were `null`, or if the
 *      instantiated expression is not based on the original one
 */
const(char)* visualizeStaticCondition(Expression original, Expression instantiated,
    const Expression[] negatives, bool full, ref uint itemCount)
{
    if (!original || !instantiated || original.loc !is instantiated.loc)
        return null;

    OutBuffer buf;

    if (full)
        itemCount = visualizeFull(original, instantiated, negatives, buf);
    else
        itemCount = visualizeShort(original, instantiated, negatives, buf);

    return buf.extractChars();
}

private uint visualizeFull(Expression original, Expression instantiated,
    const Expression[] negatives, ref OutBuffer buf)
{
    // tree-like structure; traverse and format simultaneously
    uint count;
    uint indent;

    static void printOr(uint indent, ref OutBuffer buf)
    {
        buf.reserve(indent * 4 + 8);
        foreach (i; 0 .. indent)
            buf.writestring("    ");
        buf.writestring("    or:\n");
    }

    // returns true if satisfied
    bool impl(Expression orig, Expression e, bool inverted, bool orOperand, bool unreached)
    {
        EXP op = orig.op;

        // lower all 'not' to the bottom
        // !(A && B) -> !A || !B
        // !(A || B) -> !A && !B
        if (inverted)
        {
            if (op == EXP.andAnd)
                op = EXP.orOr;
            else if (op == EXP.orOr)
                op = EXP.andAnd;
        }

        if (op == EXP.not)
        {
            NotExp no = cast(NotExp)orig;
            NotExp ne = cast(NotExp)e;
            assert(ne);
            return impl(no.e1, ne.e1, !inverted, orOperand, unreached);
        }
        else if (op == EXP.andAnd)
        {
            BinExp bo = cast(BinExp)orig;
            BinExp be = cast(BinExp)e;
            assert(be);
            const r1 = impl(bo.e1, be.e1, inverted, false, unreached);
            const r2 = impl(bo.e2, be.e2, inverted, false, unreached || !r1);
            return r1 && r2;
        }
        else if (op == EXP.orOr)
        {
            if (!orOperand) // do not indent A || B || C twice
                indent++;
            BinExp bo = cast(BinExp)orig;
            BinExp be = cast(BinExp)e;
            assert(be);
            const r1 = impl(bo.e1, be.e1, inverted, true, unreached);
            printOr(indent, buf);
            const r2 = impl(bo.e2, be.e2, inverted, true, unreached);
            if (!orOperand)
                indent--;
            return r1 || r2;
        }
        else if (op == EXP.question)
        {
            CondExp co = cast(CondExp)orig;
            CondExp ce = cast(CondExp)e;
            assert(ce);
            if (!inverted)
            {
                // rewrite (A ? B : C) as (A && B || !A && C)
                if (!orOperand)
                    indent++;
                const r1 = impl(co.econd, ce.econd, inverted, false, unreached);
                const r2 = impl(co.e1, ce.e1, inverted, false, unreached || !r1);
                printOr(indent, buf);
                const r3 = impl(co.econd, ce.econd, !inverted, false, unreached);
                const r4 = impl(co.e2, ce.e2, inverted, false, unreached || !r3);
                if (!orOperand)
                    indent--;
                return r1 && r2 || r3 && r4;
            }
            else
            {
                // rewrite !(A ? B : C) as (!A || !B) && (A || !C)
                if (!orOperand)
                    indent++;
                const r1 = impl(co.econd, ce.econd, inverted, false, unreached);
                printOr(indent, buf);
                const r2 = impl(co.e1, ce.e1, inverted, false, unreached);
                const r12 = r1 || r2;
                const r3 = impl(co.econd, ce.econd, !inverted, false, unreached || !r12);
                printOr(indent, buf);
                const r4 = impl(co.e2, ce.e2, inverted, false, unreached || !r12);
                if (!orOperand)
                    indent--;
                return (r1 || r2) && (r3 || r4);
            }
        }
        else // 'primitive' expression
        {
            buf.reserve(indent * 4 + 4);
            foreach (i; 0 .. indent)
                buf.writestring("    ");

            // find its value; it may be not computed, if there was a short circuit,
            // but we handle this case with `unreached` flag
            bool value = true;
            if (!unreached)
            {
                foreach (fe; negatives)
                {
                    if (fe is e)
                    {
                        value = false;
                        break;
                    }
                }
            }
            // write the marks first
            const satisfied = inverted ? !value : value;
            if (!satisfied && !unreached)
                buf.writestring("  > ");
            else if (unreached)
                buf.writestring("  - ");
            else
                buf.writestring("    ");
            // then the expression itself
            if (inverted)
                buf.writeByte('!');
            buf.writestring(orig.toChars);
            buf.writenl();
            count++;
            return satisfied;
        }
    }

    impl(original, instantiated, false, true, false);
    return count;
}

private uint visualizeShort(Expression original, Expression instantiated,
    const Expression[] negatives, ref OutBuffer buf)
{
    // simple list; somewhat similar to long version, so no comments
    // one difference is that it needs to hold items to display in a stack

    static struct Item
    {
        Expression orig;
        bool inverted;
    }

    Array!Item stack;

    bool impl(Expression orig, Expression e, bool inverted)
    {
        EXP op = orig.op;

        if (inverted)
        {
            if (op == EXP.andAnd)
                op = EXP.orOr;
            else if (op == EXP.orOr)
                op = EXP.andAnd;
        }

        if (op == EXP.not)
        {
            NotExp no = cast(NotExp)orig;
            NotExp ne = cast(NotExp)e;
            assert(ne);
            return impl(no.e1, ne.e1, !inverted);
        }
        else if (op == EXP.andAnd)
        {
            BinExp bo = cast(BinExp)orig;
            BinExp be = cast(BinExp)e;
            assert(be);
            bool r = impl(bo.e1, be.e1, inverted);
            r = r && impl(bo.e2, be.e2, inverted);
            return r;
        }
        else if (op == EXP.orOr)
        {
            BinExp bo = cast(BinExp)orig;
            BinExp be = cast(BinExp)e;
            assert(be);
            const lbefore = stack.length;
            bool r = impl(bo.e1, be.e1, inverted);
            r = r || impl(bo.e2, be.e2, inverted);
            if (r)
                stack.setDim(lbefore); // purge added positive items
            return r;
        }
        else if (op == EXP.question)
        {
            CondExp co = cast(CondExp)orig;
            CondExp ce = cast(CondExp)e;
            assert(ce);
            if (!inverted)
            {
                const lbefore = stack.length;
                bool a = impl(co.econd, ce.econd, inverted);
                a = a && impl(co.e1, ce.e1, inverted);
                bool b;
                if (!a)
                {
                    b = impl(co.econd, ce.econd, !inverted);
                    b = b && impl(co.e2, ce.e2, inverted);
                }
                const r = a || b;
                if (r)
                    stack.setDim(lbefore);
                return r;
            }
            else
            {
                bool a;
                {
                    const lbefore = stack.length;
                    a = impl(co.econd, ce.econd, inverted);
                    a = a || impl(co.e1, ce.e1, inverted);
                    if (a)
                        stack.setDim(lbefore);
                }
                bool b;
                if (a)
                {
                    const lbefore = stack.length;
                    b = impl(co.econd, ce.econd, !inverted);
                    b = b || impl(co.e2, ce.e2, inverted);
                    if (b)
                        stack.setDim(lbefore);
                }
                return a && b;
            }
        }
        else // 'primitive' expression
        {
            bool value = true;
            foreach (fe; negatives)
            {
                if (fe is e)
                {
                    value = false;
                    break;
                }
            }
            const satisfied = inverted ? !value : value;
            if (!satisfied)
                stack.push(Item(orig, inverted));
            return satisfied;
        }
    }

    impl(original, instantiated, false);

    foreach (i; 0 .. stack.length)
    {
        // write the expression only
        buf.writestring("       ");
        if (stack[i].inverted)
            buf.writeByte('!');
        buf.writestring(stack[i].orig.toChars);
        // here with no trailing newline
        if (i + 1 < stack.length)
            buf.writenl();
    }
    return cast(uint)stack.length;
}
