// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Lift expressions out of statements
//
// Code available from: https://verilator.org
//
//*************************************************************************
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of either the GNU Lesser General Public License Version 3
// or the Perl Artistic License Version 2.0.
// SPDX-FileCopyrightText: 2003-2026 Wilson Snyder
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
//
//*************************************************************************
//
// V3LiftExpr's Transformations:
//
// Lift impure sub-expressions and function calls out of expressions,
// turning them into additional statements. This has several benefits
// that enable further downstream optimizations:
// - Impure expressions always appear on the RHS of a simple assignment,
//   reducing the size of impure expressions. This also eliminates
//   later needs for cloning impure expressions, preserving side effects
// - Lifted function calls can be inlined without the use of AstExprStmt,
//   which is poorly handled by optimizations, especially Dfg
// - Reduces complexity of downstream lowering passes as they need to deal
//   with fewer special cases.
//
// The generic transformation applies for all AstNodeStmt. Using AstAssign
// as an example:
//    x[impure_x] = impure_y + func(impure_z);
// is transformed into:
//    __VleImpure_0 = impure_y;
//    __VleImpure_1 = impure_z;
//    __VleCall_0 = func(__VleImpure_1);
//    __VleImpure_2 = impure_x;
//    x[__VleImpure_2] = __VleImpure_0 + __VleCall_0;
// All parts of the assignment is now pure, and the function call can be
// inlined by V3Task without the use of AstExprStmt.
//
// Care must be taken for the 4 short-circuiting operators: && || -> ?:
// For example AstLogAnd:
//    z = x && func(y)
// is transformed into:
//    __VleLogAnd_0 = x;
//    if (__VleLogAnd_0) {
//      __VleCall_0 = func(y);
//      __VleLogAnd_0 = __VleCall_0;
//    }
//    z = __VleLogAnd_1;
// Similar patterns are used for AstLogOr and AstCond to preserve the
// short-circuiting semantics and side effects. All AstLogIf should have
// been converted to AstLogOr earlier by V3Const.
//
// Care must be taken for impure LValues as well. While, all LValue expressions
// permitted by IEEE-1800 are themselves pure (except possibly for the non-lvalue
// sub-expressions like array indices, which are not a problem), we support a
// non-standard but common extension where a function call returning a class
// handle, under a member select can be an LValue, e.g.:
//    'getInstance().member = foo;'
// Fortunately we can discover these via MemberSel, and we can transform them
// to the IEEE-1800 compliant form:
//    __VleLvalCall_0 = getInstance();
//    __VleLvalCall_0.member = foo;
// There are also some other internal LValues represented via CMethodHard that
// return references, and are marked as impure. For this reason only we still
// need to special case their handling via AstNodeExpr::isLValue().
//
//*************************************************************************

#include "V3PchAstNoMT.h"  // VL_MT_DISABLED_CODE_UNIT

#include "verilatedos.h"

#include "V3LiftExpr.h"

#include "V3Ast.h"
#include "V3Error.h"
#include "V3FileLine.h"
#include "V3Inst.h"
#include "V3Stats.h"

VL_DEFINE_DEBUG_FUNCTIONS;

//######################################################################

class LiftExprVisitor final : public VNVisitor {
    // NODE STATE
    // AstNodeStmt::user1()  -> bool. Statement already processed
    // AstVar::user1()       -> bool. Variable is a lifted temporary
    // AstNodeExpr::user1p() -> AstVar*. Existing temporary variable usable for this expression
    const VNUser1InUse m_user1InUse;

    // STATE
    AstNodeModule* m_modp = nullptr;  // Current module
    AstNodeFTask* m_ftaskp = nullptr;  // Current function/task
    bool m_lift = false;  // Lift encountered expressions
    // Statements lifted out of current node. TODO: Make this an AstNodeStmt* after #6280
    AstNode* m_newStmtps = nullptr;
    // Expressions in some special locations need not, or should not be lifted
    AstNodeExpr* m_doNotLiftp = nullptr;
    size_t m_nTmps = 0;  // Sequence numbers for temporary variables
    // Statistics
    VDouble0 m_statLiftedExprs;
    VDouble0 m_statLiftedCalls;
    VDouble0 m_statLiftedLvalCalls;
    VDouble0 m_statLiftedConds;
    VDouble0 m_statLiftedLogAnds;
    VDouble0 m_statLiftedLogOrs;
    VDouble0 m_statLiftedExprStmts;
    VDouble0 m_statTemporariesCreated;
    VDouble0 m_statTemporariesReused;

    // METHODS
    AstVar* newVar(const char* baseName, const AstNodeExpr* exprp,
                   const std::string& suffix = "") {
        // Reuse existing temporary if available
        if (exprp->user1p()) {
            ++m_statTemporariesReused;
            return VN_AS(exprp->user1p(), Var);
        }

        // Need a separate name in functions vs modules, as module inlining
        // can otherwise cause spurious VARHIDDEN warnings
        std::string name = m_ftaskp ? "__Vlef" : "__Vlem";
        name += baseName;
        name += "_" + std::to_string(m_nTmps++);
        if (!suffix.empty()) name += "__" + suffix;

        // Create variable
        ++m_statTemporariesCreated;
        AstVar* const varp = new AstVar{exprp->fileline(), VVarType::MODULETEMP, name,
                                        exprp->dtypep()->skipRefp()};
        varp->isInternal(true);
        varp->noReset(true);
        varp->user1(1);  // Mark as lifted temporary so it can be reused

        if (m_ftaskp) {
            varp->funcLocal(true);
            m_ftaskp->stmtsp()->addHereThisAsNext(varp);
            varp->lifetime(VLifetime::AUTOMATIC_EXPLICIT);
        } else {
            m_modp->stmtsp()->addHereThisAsNext(varp);
            // 'automatic' on a variable inside a class actually means 'member'
            varp->lifetime(VN_IS(m_modp, Class) ? VLifetime::STATIC_EXPLICIT
                                                : VLifetime::AUTOMATIC_EXPLICIT);
        }
        return varp;
    }

    // If the expression is a reference to an existing temporary, return it, otherwise nullptr
    AstVar* getExistingVar(AstNodeExpr* nodep) {
        if (AstVarRef* const refp = VN_CAST(nodep, VarRef)) {
            if (refp->varp()->user1()) return refp->varp();
        }
        return nullptr;
    }

    // Unlink and assign given expression to temporary, unless the expression
    // is a reference to the same temporary
    AstAssign* assignIfDifferent(FileLine* flp, AstVar* varp, AstNodeExpr* nodep) {
        if (AstVarRef* const refp = VN_CAST(nodep, VarRef)) {
            if (refp->varp() == varp) return nullptr;
        }
        return new AstAssign{flp, new AstVarRef{flp, varp, VAccess::WRITE}, nodep->unlinkFrBack()};
    }

    void addStmtps(AstNode* stmtp) {
        if (!stmtp) return;
        // No need to process again, so mark
        for (AstNode* nodep = stmtp; nodep; nodep = nodep->nextp()) nodep->user1(1);
        m_newStmtps = AstNode::addNext(m_newStmtps, stmtp);
    }

    // Lift expressions from expression, return lifted statements
    AstNode* lift(AstNodeExpr* nodep) {
        if (!nodep) return nullptr;
        VL_RESTORER(m_lift);
        VL_RESTORER(m_newStmtps);
        m_lift = true;
        m_newStmtps = nullptr;
        iterate(nodep);
        return m_newStmtps;
    }

    // Lift expressions from children of given statement, return lifted statements
    AstNode* liftChildren(AstNodeStmt* nodep) {
        VL_RESTORER(m_lift);
        VL_RESTORER(m_newStmtps);
        m_lift = true;
        m_newStmtps = nullptr;
        iterateChildren(nodep);
        return m_newStmtps;
    }

    // VISITORS - non-statement, non-expression
    void visit(AstNode* nodep) override {
        VL_RESTORER(m_lift);
        m_lift = false;  // Conservatively do not lift if unknown construct
        iterateChildren(nodep);
    }
    void visit(AstNodeModule* nodep) override {
        // Reset names on root module only (there can be classes nested in modules)
        if (!m_modp) m_nTmps = 0;
        VL_RESTORER(m_modp);
        m_modp = nodep;
        iterateChildren(nodep);
    }
    void visit(AstNodeFTask* nodep) override {
        VL_RESTORER(m_ftaskp);
        VL_RESTORER(m_nTmps);
        m_ftaskp = nodep;
        m_nTmps = 0;
        iterateChildren(nodep);
    }
    void visit(AstCaseItem* nodep) override {
        // Do not lift from the case expressions, there is nowhere to put them
        iterateAndNextNull(nodep->stmtsp());
    }
    void visit(AstCell* nodep) override {
        // No need to fix up port connections, V3Tristate called pinReconnectSimple,
        // so all writes are to simple AstVars, assuming it did it right ...
    }
    void visit(AstAlias* nodep) override {}
    void visit(AstArg* nodep) override {
        // Lift argument expressions
        iterateChildren(nodep);
    }

    // VISITORS - statements
    void visit(AstNodeStmt* nodep) override {
        if (nodep->user1SetOnce()) return;
        VL_RESTORER(m_doNotLiftp);
        m_doNotLiftp = nullptr;
        if (AstNode* const newStmtps = liftChildren(nodep)) nodep->addHereThisAsNext(newStmtps);
    }
    void visit(AstNodeAssign* nodep) override {
        if (nodep->user1SetOnce()) return;
        VL_RESTORER(m_doNotLiftp);
        // Do not lift the RHS if this is already a simple assignment to a variable
        m_doNotLiftp = VN_IS(nodep->lhsp(), NodeVarRef) ? nodep->rhsp() : nullptr;
        if (AstNode* const newStmtps = lift(nodep->rhsp())) {
            nodep->addHereThisAsNext(newStmtps);
        }

        if (nodep->timingControlp()) return;

        if (AstNode* const newStmtps = lift(nodep->lhsp())) {
            nodep->addHereThisAsNext(newStmtps);
        }
    }
    void visit(AstStmtExpr* nodep) override {
        if (nodep->user1SetOnce()) return;
        // Ignore super class constructor calls - can't insert statements before them
        if (VN_IS(nodep->exprp(), New)) return;
        VL_RESTORER(m_doNotLiftp);
        // Do not lift if the expression itself. This AstStmtExpr is required to
        // throw away the return value if any, and V3Task can inline without using
        // AstExprStmt in this case. Can still lift all sub-expressions though.
        m_doNotLiftp = nodep->exprp();
        if (AstNode* const newStmtps = liftChildren(nodep)) nodep->addHereThisAsNext(newStmtps);
    }
    // Don't know whether these are sensitive to lifting, assume so
    void visit(AstCStmt* nodep) override {}
    void visit(AstCStmtUser* nodep) override {}

    // VISITORS - expressions
    void visit(AstNodeExpr* nodep) override {
        if (!m_lift) return;

        iterateChildren(nodep);

        // Do not lift if already in normal form
        if (m_doNotLiftp == nodep) return;
        // No need to lift void expressions, these should be under StmtExpr, but just in case ...
        if (VN_IS(nodep->dtypep()->skipRefp(), VoidDType)) return;
        // Do not lift if pure
        if (nodep->isPure()) return;
        // Do not lift if LValue
        if (nodep->isLValue()) return;

        // Extract expression into a temporary variable
        ++m_statLiftedExprs;
        FileLine* const flp = nodep->fileline();
        AstVar* const varp = newVar("Expr", nodep);
        nodep->replaceWith(new AstVarRef{flp, varp, VAccess::READ});
        addStmtps(new AstAssign{flp, new AstVarRef{flp, varp, VAccess::WRITE}, nodep});
    }
    void visit(AstNodeFTaskRef* nodep) override {
        if (!m_lift) return;

        iterateChildren(nodep);

        // Do not lift if already in normal form
        if (m_doNotLiftp == nodep) return;
        // No need to lift void functions, these should be under StmtExpr, but just in case ...
        if (VN_IS(nodep->dtypep()->skipRefp(), VoidDType)) return;
        // Do not lift Taskref, it's always in statement position and cleanly inlineable.
        if (VN_IS(nodep, TaskRef)) return;

        // Extract expression into a temporary variable
        ++m_statLiftedCalls;
        FileLine* const flp = nodep->fileline();
        AstVar* const varp = newVar("Call", nodep, nodep->taskp()->name());
        nodep->replaceWith(new AstVarRef{flp, varp, VAccess::READ});
        addStmtps(new AstAssign{flp, new AstVarRef{flp, varp, VAccess::WRITE}, nodep});
    }
    void visit(AstMemberSel* nodep) override {
        if (!m_lift) return;
        // MemberSel is special as it can appear as an LValue selecting from a call
        // that returns a class handle. Note this is not in IEEE-1800, but is supported
        // for compatibility. Here we turn it into an IEEE-1800 compliant LValue by
        // lifting the call and use the return value via a temporary variable.

        VL_RESTORER(m_doNotLiftp);
        // If it's an LValue call, do not lift the fromp, will do it explicitly below
        m_doNotLiftp = nodep->access().isWriteOrRW() && VN_IS(nodep->fromp(), NodeFTaskRef)
                           ? nodep->fromp()
                           : nullptr;
        iterateChildren(nodep);
        // Done if not a special case
        if (!m_doNotLiftp) return;

        // Extract LValue call into a temporary variable
        ++m_statLiftedLvalCalls;
        FileLine* const flp = nodep->fileline();
        AstVar* const varp = newVar("LvalCall", nodep->fromp());
        addStmtps(new AstAssign{flp, new AstVarRef{flp, varp, VAccess::WRITE},
                                nodep->fromp()->unlinkFrBack()});
        // This one is a WRITE or READWRITE, same as the MemberSel
        nodep->fromp(new AstVarRef{flp, varp, nodep->access()});
    }

    // VISITORS - RValue expressions
    void visit(AstCond* nodep) override {
        if (!m_lift) return;

        // Lift from condition
        iterate(nodep->condp());

        // Temporary variable to use for this expression
        AstVar* varp = nullptr;

        const bool sameType
            = nodep->thenp()->dtypep()->skipRefp()->sameTree(nodep->elsep()->dtypep()->skipRefp());

        // Lift from the Then branch
        AstNode* const thenStmtps = lift(nodep->thenp());
        // If lifted the Then branch, we can reuse the temporary if same type
        if (sameType) varp = getExistingVar(nodep->thenp());
        if (varp) nodep->elsep()->user1p(varp);

        // Lift from the Else branch
        AstNode* const elseStmtps = lift(nodep->elsep());
        // If lifted the Else branch, we can reuse the temporary if same type
        if (sameType && !varp) varp = getExistingVar(nodep->elsep());

        // If nothing lifted, then nothing to do
        if (!thenStmtps && !elseStmtps) return;

        // Otherwise convert to an AstIf with a temporary variable
        ++m_statLiftedConds;
        FileLine* const flp = nodep->fileline();
        if (!varp) varp = newVar("Cond", nodep);
        // Create the AstIf
        AstIf* const ifp = new AstIf{flp, nodep->condp()->unlinkFrBack()};
        addStmtps(ifp);
        ifp->addThensp(thenStmtps);
        ifp->addThensp(assignIfDifferent(flp, varp, nodep->thenp()));
        ifp->addElsesp(elseStmtps);
        ifp->addElsesp(assignIfDifferent(flp, varp, nodep->elsep()));
        // Replace the expression with a reference to the temporary variable
        nodep->replaceWith(new AstVarRef{flp, varp, VAccess::READ});
        VL_DO_DANGLING(nodep->deleteTree(), nodep);
    }
    void visit(AstLogAnd* nodep) override {
        if (!m_lift) return;

        // Lift from LHS
        iterate(nodep->lhsp());

        // Temporary variable to use for this expression
        AstVar* varp = getExistingVar(nodep->lhsp());
        // If lifted the LHS, we can reuse the temporary
        if (varp) nodep->rhsp()->user1p(varp);

        // Lift from RHS, if nothing lifted, then nothing to do
        AstNode* const rhsStmtps = lift(nodep->rhsp());
        if (!rhsStmtps) return;

        // Otherwise convert to an AstIf with a temporary variable
        ++m_statLiftedLogAnds;
        FileLine* const flp = nodep->fileline();
        if (!varp) varp = getExistingVar(nodep->rhsp());
        if (!varp) varp = newVar("LogAnd", nodep);
        addStmtps(assignIfDifferent(flp, varp, nodep->lhsp()));
        AstIf* const ifp = new AstIf{flp, new AstVarRef{flp, varp, VAccess::READ}};
        addStmtps(ifp);
        ifp->addThensp(rhsStmtps);
        ifp->addThensp(assignIfDifferent(flp, varp, nodep->rhsp()));
        nodep->replaceWith(new AstVarRef{flp, varp, VAccess::READ});
        VL_DO_DANGLING(nodep->deleteTree(), nodep);
    }
    void visit(AstLogOr* nodep) override {
        if (!m_lift) return;

        // Lift from LHS
        iterate(nodep->lhsp());

        // Temporary variable to use for this expression
        AstVar* varp = getExistingVar(nodep->lhsp());
        // If lifted the LHS, we can reuse the temporary
        if (varp) nodep->rhsp()->user1p(varp);

        // Lift from RHS, if nothing lifted, then nothing to do
        AstNode* const rhsStmtps = lift(nodep->rhsp());
        if (!rhsStmtps) return;

        // Otherwise convert to an AstIf with a temporary variable
        ++m_statLiftedLogOrs;
        FileLine* const flp = nodep->fileline();
        if (!varp) varp = getExistingVar(nodep->rhsp());
        if (!varp) varp = newVar("LogOr", nodep);
        addStmtps(assignIfDifferent(flp, varp, nodep->lhsp()));
        AstIf* const ifp = new AstIf{flp, new AstVarRef{flp, varp, VAccess::READ}};
        addStmtps(ifp);
        ifp->addElsesp(rhsStmtps);
        ifp->addElsesp(assignIfDifferent(flp, varp, nodep->rhsp()));
        nodep->replaceWith(new AstVarRef{flp, varp, VAccess::READ});
        VL_DO_DANGLING(nodep->deleteTree(), nodep);
    }
    void visit(AstLogIf* nodep) override {
        if (!m_lift) return;
        nodep->v3fatalSrc("AstLogIf should have been folded by V3Const");
    }
    void visit(AstExprStmt* nodep) override {
        if (!m_lift) return;

        // Eliminate AstExprStmt by lifting the content entirely
        ++m_statLiftedExprStmts;
        iterate(nodep->stmtsp());
        addStmtps(nodep->stmtsp()->unlinkFrBackWithNext());
        iterate(nodep->resultp());
        nodep->replaceWith(nodep->resultp()->unlinkFrBack());
        VL_DO_DANGLING(nodep->deleteTree(), nodep);
    }

    // VISITORS - Accelerate pure leaf expressions
    void visit(AstConst*) override {}
    void visit(AstVarRef*) override {}
    void visit(AstVarXRef*) override {}

    // VISITORS - Expression special cases
    // These return C++ references rather than values, cannot be lifted
    void visit(AstAssocSel* nodep) override { iterateChildren(nodep); }
    void visit(AstCMethodHard* nodep) override { iterateChildren(nodep); }
    // Don't know whether these may return non-values, assume so
    void visit(AstCExpr* nodep) override { iterateChildren(nodep); }
    void visit(AstCExprUser* nodep) override { iterateChildren(nodep); }

public:
    // CONSTRUCTORS
    explicit LiftExprVisitor(AstNetlist* nodep) {
        // Extracting expressions can effect purity
        VIsCached::clearCacheTree();
        iterate(nodep);
        VIsCached::clearCacheTree();
        if (m_newStmtps) m_newStmtps->dumpTreeAndNext(std::cout, "Leftover:");
        UASSERT_OBJ(!m_newStmtps, m_newStmtps, "Failed to insert statements");
    }
    ~LiftExprVisitor() override {
        V3Stats::addStat("LiftExpr, lifted impure expressions", m_statLiftedExprs);
        V3Stats::addStat("LiftExpr, lifted calls", m_statLiftedCalls);
        V3Stats::addStat("LiftExpr, lifted calls as lvalue", m_statLiftedLvalCalls);
        V3Stats::addStat("LiftExpr, lifted Cond", m_statLiftedConds);
        V3Stats::addStat("LiftExpr, lifted LogAnd", m_statLiftedLogAnds);
        V3Stats::addStat("LiftExpr, lifted LogOr", m_statLiftedLogOrs);
        V3Stats::addStat("LiftExpr, lifted ExprStmt", m_statLiftedExprStmts);
        V3Stats::addStat("LiftExpr, temporaries created", m_statTemporariesCreated);
        V3Stats::addStat("LiftExpr, temporaries reused", m_statTemporariesReused);
    }
};

//######################################################################
// Unknown class functions

void V3LiftExpr::liftExprAll(AstNetlist* nodep) {
    UINFO(2, __FUNCTION__ << ":");
    { LiftExprVisitor{nodep}; }
    V3Global::dumpCheckGlobalTree("lift_expr", 0, dumpTreeEitherLevel() >= 3);
}
