// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Add temporaries, such as for premit nodes
//
// 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
//
//*************************************************************************
// V3Premit's Transformations:
//
// Each module:
//      For each wide OP, make a a temporary variable with the wide value
//      For each deep expression, assign expression to temporary.
//
// Each display (independent transformation; here as Premit is a good point)
//      If autoflush, insert a flush
//
//*************************************************************************

#include "V3PchAstNoMT.h"  // VL_MT_DISABLED_CODE_UNIT

#include "V3Premit.h"

#include "V3Stats.h"
#include "V3UniqueNames.h"

VL_DEFINE_DEBUG_FUNCTIONS;

constexpr int STATIC_CONST_MIN_WIDTH = 256;  // Minimum size to extract to static constant

//######################################################################
// Premit state, as a visitor of each AstNode

class PremitVisitor final : public VNVisitor {
    // NODE STATE
    //  AstNodeExpr::user()     -> bool.  True if iterated already
    //  *::user3()              -> Used when visiting AstNodeAssign
    const VNUser1InUse m_inuser1;

    // STATE - across all visitors
    VDouble0 m_extractedToConstPool;  // Statistic tracking
    VDouble0 m_temporaryVarsCreated;  // Statistic tracking

    // STATE - for current visit position (use VL_RESTORER)
    AstCFunc* m_cfuncp = nullptr;  // Current block
    size_t m_tmpVarCnt = 0;  // Number of temporary variables created inside a function
    AstNode* m_stmtp = nullptr;  // Current statement
    bool m_assignLhs = false;  // Inside assignment lhs, don't breakup extracts

    // METHODS
    bool needsTemp(AstNodeExpr* nodep) {
        // Consider adding a temp for this expression.
        if (!m_stmtp) return false;  // Not under a statement
        if (nodep->user1SetOnce()) return false;  // Already processed
        if (!nodep->isWide()) return false;  // Not wide
        if (m_assignLhs) return false;  // This is an lvalue!
        UASSERT_OBJ(!VN_IS(nodep->firstAbovep(), ArraySel), nodep, "Should have been ignored");
        return true;
    }

    void checkNode(AstNodeExpr* nodep) {
        if (needsTemp(nodep)) createTemp(nodep);
    }

    AstVarRef* isRhsOfAssignToVar(AstNodeExpr* nodep) {
        // If enclosing statement is an assign
        AstNodeAssign* const assignp = VN_CAST(m_stmtp, NodeAssign);
        if (!assignp) return nullptr;
        // of which 'nodep' is the RHS of
        if (assignp->rhsp() != nodep) return nullptr;
        // and the LHS is a sipmple variable reference
        AstVarRef* const lRefp = VN_CAST(assignp->lhsp(), VarRef);
        if (!lRefp) return nullptr;
        AstVar* const varp = lRefp->varp();
        // And the RHS does not use the same variable
        if (nodep->exists([varp](const AstVarRef* refp) { return refp->varp() == varp; })) {
            return nullptr;
        }
        // Then return the LHS reference
        return lRefp;
    }

    // Create a new temporary that can hold the value of the given expression
    AstVar* newTmpFor(AstNodeExpr* nodep) {
        FileLine* const flp = nodep->fileline();
        const std::string name = "__Vtemp_" + std::to_string(++m_tmpVarCnt);
        AstVar* const varp = new AstVar{flp, VVarType::STMTTEMP, name, nodep->dtypep()};
        varp->funcLocal(true);
        varp->noReset(true);
        m_cfuncp->addVarsp(varp);
        ++m_temporaryVarsCreated;
        return varp;
    }

    AstVar* createTemp(AstNodeExpr* nodep) {
        UASSERT_OBJ(m_stmtp, nodep, "Attempting to create temporary with no insertion point");
        UINFO(4, "createTemp: " << nodep);

        VNRelinker relinker;
        nodep->unlinkFrBack(&relinker);

        FileLine* const flp = nodep->fileline();
        AstConst* const constp = VN_CAST(nodep, Const);
        const bool useConstPool = constp  // Is a constant
                                  && (constp->width() >= STATIC_CONST_MIN_WIDTH)  // Large enough
                                  && !constp->num().isFourState()  // Not four state
                                  && !constp->num().isString();  // Not a string

        AstVar* varp = nullptr;

        if (useConstPool) {
            // Extract into constant pool.
            const bool merge = v3Global.opt.fMergeConstPool();
            varp = v3Global.rootp()->constPoolp()->findConst(constp, merge)->varp();
            VL_DO_DANGLING(nodep->deleteTree(), nodep);
            ++m_extractedToConstPool;
        } else {
            // Keep as local temporary.
            varp = newTmpFor(nodep);
            // Assignment to put before the referencing statement
            AstVarRef* const refp = new AstVarRef{flp, varp, VAccess::WRITE};
            AstAssign* const assignp = new AstAssign{flp, refp, nodep};
            // Insert before the statement
            m_stmtp->addHereThisAsNext(assignp);
        }

        // Replace node with VarRef to new Var
        relinker.relink(new AstVarRef{flp, varp, VAccess::READ});

        // Return the temporary variable
        return varp;
    }

    void visitShift(AstNodeBiop* nodep) {
        UINFO(4, "  ShiftFix  " << nodep);
        UASSERT_OBJ(VN_IS(nodep, ShiftL) || VN_IS(nodep, ShiftR) || VN_IS(nodep, ShiftRS), nodep,
                    "Bad case");
        // Shift larger than the width of the type (overshift) is undefined behavour in C++
        // (in practice will shift by the wrapped shift amount). These are requierd to go to
        // zero/msbs, so replacing them here.
        FileLine* const flp = nodep->fileline();
        if (const AstConst* const shiftp = VN_CAST(nodep->rhsp(), Const)) {
            // Shift amount known to be constant. If oversized shift, replace with zero/msbs.
            // Otherwise we can leave the original shifts which have better constant folding
            // than the *Ovr versions.
            const bool isOversized = shiftp->num().mostSetBitP1() > 32  //
                                     || (shiftp->num().toSQuad() >= nodep->width());
            if (isOversized) {
                AstNodeExpr* newp = nullptr;
                if (VN_IS(nodep, ShiftRS)) {
                    AstNodeExpr* const lhsp = nodep->lhsp()->unlinkFrBack();
                    AstNodeExpr* const msbp = new AstSel{flp, lhsp, nodep->width() - 1, 1};
                    newp = new AstExtendS{flp, msbp, nodep->width()};
                } else {
                    newp = new AstConst{flp, AstConst::DTyped{}, nodep->dtypep()};
                }
                nodep->replaceWithKeepDType(newp);
                VL_DO_DANGLING(pushDeletep(nodep), nodep);
                return;
            }
        } else {
            // Shift amount not known at compile time. Convert to *Ovr version. Don't need to do
            // if it would use a wide operation which works correctly at runtime, of if the max
            // value of the shift amount is less than the with of the shifted value.
            if (nodep->widthMin() <= VL_QUADSIZE
                && (nodep->width() < (1LL << nodep->rhsp()->widthMin()))) {
                AstNodeExpr* const lhsp = nodep->lhsp()->unlinkFrBack();
                AstNodeExpr* const rhsp = nodep->rhsp()->unlinkFrBack();
                AstNodeExpr* newp = nullptr;
                if (VN_IS(nodep, ShiftL)) {
                    newp = new AstShiftLOvr{flp, lhsp, rhsp};
                } else if (VN_IS(nodep, ShiftR)) {
                    newp = new AstShiftROvr{flp, lhsp, rhsp};
                } else {
                    newp = new AstShiftRSOvr{flp, lhsp, rhsp};
                }
                nodep->replaceWithKeepDType(newp);
                VL_DO_DANGLING(pushDeletep(nodep), nodep);
                return;
            }
        }
        iterateChildren(nodep);
        checkNode(nodep);
    }

    static bool rhsReadsLhs(const AstNodeAssign* nodep) {
        const VNUser3InUse user3InUse;
        nodep->lhsp()->foreach([](const AstVarRef* refp) {
            if (refp->access().isWriteOrRW()) refp->varp()->user3(true);
        });
        return nodep->rhsp()->exists([](const AstVarRef* refp) {
            return refp->access().isReadOnly() && refp->varp()->user3();
        });
    }

    // VISITORS
    void visit(AstCFunc* nodep) override {
        UASSERT_OBJ(!m_cfuncp, nodep, "Should not nest");
        VL_RESTORER(m_cfuncp);
        VL_RESTORER(m_tmpVarCnt);
        m_cfuncp = nodep;
        m_tmpVarCnt = 0;
        iterateChildren(nodep);
    }

    // VISITORS - Statements
#define START_STATEMENT_OR_RETURN(stmtp) \
    if (!m_cfuncp) return; \
    if ((stmtp)->user1SetOnce()) return; \
    VL_RESTORER(m_assignLhs); \
    VL_RESTORER(m_stmtp); \
    m_assignLhs = false; \
    m_stmtp = (stmtp);

    void visit(AstNodeAssign* nodep) override {
        START_STATEMENT_OR_RETURN(nodep);

        if (AstCvtArrayToPacked* const packedp = VN_CAST(nodep->lhsp(), CvtArrayToPacked)) {
            // AstCvtArrayToPacked is converted to VL_PACK, which returns rvalue,
            // so it shouldn't be on the LHS. It is now replaced with unpacking of RHS.
            AstNodeExpr* const exprLhsp = packedp->fromp()->unlinkFrBack();
            packedp->replaceWith(exprLhsp);
            VL_DO_DANGLING(pushDeletep(packedp), packedp);
            AstNodeExpr* const rhsp = nodep->rhsp()->unlinkFrBack();
            nodep->rhsp(new AstCvtPackedToArray{rhsp->fileline(), rhsp, exprLhsp->dtypep()});
            nodep->dtypeFrom(exprLhsp);
        }
        // Direct assignment to a simple variable
        if (VN_IS(nodep->lhsp(), VarRef) && !AstVar::scVarRecurse(nodep->lhsp())) {
            AstNode* const rhsp = nodep->rhsp();
            // Rhs is already a var ref, so nothing to do
            if (VN_IS(rhsp, VarRef) && !AstVar::scVarRecurse(rhsp)) return;
            if (VN_IS(rhsp, Cond)) {
                // Do replace Cond on RHS, even if a simple assignment
            } else if (!VN_IS(rhsp, Const)) {
                // Don't replace the rhs, it's already a simple assignment
                rhsp->user1(true);
            } else if (rhsp->width() < STATIC_CONST_MIN_WIDTH) {
                // It's a small constant, so nothing to do, otherwise will be put to const pool
                return;
            }
        }

        // If the RHS reads the LHS, we need a temporary unless the update is atomic
        const bool isAtomic = VN_IS(nodep->lhsp(), VarRef) && !nodep->lhsp()->isWide();
        if (!isAtomic && rhsReadsLhs(nodep)) {
            createTemp(nodep->rhsp());
        } else {
            iterateAndNextNull(nodep->rhsp());
        }

        m_assignLhs = true;  // Restored by VL_RESTORER in START_STATEMENT_OR_RETURN
        iterateAndNextNull(nodep->lhsp());

        // It's possible we end up with an 'a' = 'a' after expanding an AstCond
        if (AstVarRef* const rhsp = VN_CAST(nodep->rhsp(), VarRef)) {
            if (AstVarRef* const lhsp = VN_CAST(nodep->lhsp(), VarRef)) {
                if (lhsp->varp() == rhsp->varp()) {
                    VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep);
                }
            }
        }
    }
    void visit(AstDisplay* nodep) override {
        START_STATEMENT_OR_RETURN(nodep);
        iterateChildren(nodep);
        if (v3Global.opt.autoflush()) {
            const AstNode* searchp = nodep->nextp();
            while (searchp && VN_IS(searchp, Comment)) searchp = searchp->nextp();
            if (searchp && VN_IS(searchp, Display)
                && nodep->filep()->sameGateTree(VN_AS(searchp, Display)->filep())) {
                // There's another display next; we can just wait to flush
            } else {
                UINFO(4, "Autoflush " << nodep);
                nodep->addNextHere(
                    new AstFFlush{nodep->fileline(),
                                  nodep->filep() ? nodep->filep()->cloneTreePure(true) : nullptr});
            }
        }
    }
    void visit(AstNodeStmt* nodep) override {
        START_STATEMENT_OR_RETURN(nodep);
        iterateChildren(nodep);
    }

#undef START_STATEMENT_OR_RETURN

    // VISITORS - Expressions
    void visit(AstShiftL* nodep) override { visitShift(nodep); }
    void visit(AstShiftR* nodep) override { visitShift(nodep); }
    void visit(AstShiftRS* nodep) override { visitShift(nodep); }

    void visit(AstConst* nodep) override { checkNode(nodep); }
    // Operators
    void visit(AstNodeTermop* nodep) override { checkNode(nodep); }
    void visit(AstNodeUniop* nodep) override {
        iterateChildren(nodep);
        checkNode(nodep);
    }
    void visit(AstNodeBiop* nodep) override {
        iterateChildren(nodep);
        checkNode(nodep);
    }
    void visit(AstRand* nodep) override {
        iterateChildren(nodep);
        checkNode(nodep);
    }
    void visit(AstRandRNG* nodep) override {
        iterateChildren(nodep);
        checkNode(nodep);
    }
    void visit(AstCExprUser* nodep) override {
        iterateChildren(nodep);
        checkNode(nodep);
    }
    void visit(AstCMethodHard* nodep) override {
        iterateChildren(nodep);
        checkNode(nodep);
    }
    void visit(AstCvtArrayToPacked* nodep) override {
        iterateChildren(nodep);
        checkNode(nodep);
    }
    void visit(AstCvtPackedToArray* nodep) override {
        iterateChildren(nodep);
        checkNode(nodep);
        if (!VN_IS(nodep->backp(), NodeAssign)) createTemp(nodep);
    }
    void visit(AstCvtUnpackedToQueue* nodep) override {
        iterateChildren(nodep);
        checkNode(nodep);
    }
    void visit(AstSel* nodep) override {
        iterateAndNextNull(nodep->fromp());
        {  // Only the 'from' is part of the assignment LHS
            VL_RESTORER(m_assignLhs);
            m_assignLhs = false;
            iterateAndNextNull(nodep->lsbp());
            // AstSel::widthp() must remain a constant, so not iterating
        }
        checkNode(nodep);
    }
    void visit(AstArraySel* nodep) override {
        iterateAndNextNull(nodep->fromp());
        {  // Only the 'from' is part of the assignment LHS
            VL_RESTORER(m_assignLhs);
            m_assignLhs = false;
            iterateAndNextNull(nodep->bitp());
        }
        // ArraySel are just pointer arithmetic and should never be replaced
    }
    void visit(AstAssocSel* nodep) override {
        iterateAndNextNull(nodep->fromp());
        {  // Only the 'from' is part of the assignment LHS
            VL_RESTORER(m_assignLhs);
            m_assignLhs = false;
            iterateAndNextNull(nodep->bitp());
        }
        checkNode(nodep);
    }
    void visit(AstMatchMasked* nodep) override {
        iterateChildren(nodep);
        if (!nodep->user1SetOnce()) {
            // Don't want this replicated by V3Expand
            AstVar* const varp = createTemp(nodep);
            varp->noSubst(true);  // Do not re-inline in V3Subst
        }
    }
    void visit(AstCond* nodep) override {
        // Convert AstCond to AstIf in order to avoid evaluating
        // sub-expressions in both branches unconditionally.
        if (needsTemp(nodep)) {
            // Check if LHS variable could be used directly
            AstVarRef* const lRefp = isRhsOfAssignToVar(nodep);
            // If not, create a new temporary variable
            AstVar* varp = lRefp ? lRefp->varp() : newTmpFor(nodep);
            // Can't substitute across basic blocks
            varp->noSubst(true);

            FileLine* const flp = nodep->fileline();
            // Create 'then' assignment
            AstVarRef* const thenRefp = new AstVarRef{flp, varp, VAccess::WRITE};
            if (lRefp) thenRefp->selfPointer(lRefp->selfPointer());
            AstNodeExpr* const thenExprp = nodep->thenp()->unlinkFrBack();
            AstAssign* const thenAsspp = new AstAssign{flp, thenRefp, thenExprp};
            // Create 'else' assignment
            AstVarRef* const elseRefp = new AstVarRef{flp, varp, VAccess::WRITE};
            if (lRefp) elseRefp->selfPointer(lRefp->selfPointer());
            AstNodeExpr* const elseExprp = nodep->elsep()->unlinkFrBack();
            AstAssign* const elseAsspp = new AstAssign{flp, elseRefp, elseExprp};
            // Creae 'if' and insert it before the statement
            AstNodeExpr* const condp = nodep->condp()->unlinkFrBack();
            AstIf* const ifp = new AstIf{flp, condp, thenAsspp, elseAsspp};
            m_stmtp->addHereThisAsNext(ifp);
            // Replace the AstCond with the result variable
            nodep->replaceWith(new AstVarRef{flp, varp, VAccess::READ});
            VL_DO_DANGLING(pushDeletep(nodep), nodep);
            // Splitting to multiple statements can change purity
            VIsCached::clearCacheTree();
            // Iterate the resulting assignments
            iterate(ifp);
            return;
        }
        iterateChildren(nodep);
    }
    void visit(AstSFormatF* nodep) override {
        iterateChildren(nodep);
        // Any strings sent to a display must be var of string data type,
        // to avoid passing a pointer to a temporary.
        AstNodeExpr* exprsp = nodep->exprsp();
        if (nodep->exprFormat()) exprsp = VN_AS(exprsp->nextp(), NodeExpr);
        for (AstNodeExpr *argp = exprsp, *nextp; argp; argp = nextp) {
            nextp = VN_AS(argp->nextp(), NodeExpr);

            AstSFormatArg* const fargp = VN_CAST(argp, SFormatArg);
            AstNodeExpr* const subargp = fargp ? fargp->exprp() : argp;
            // Must avoid taking address of rvalue, so even Const needs a temp
            if (subargp->isString() && !VN_IS(subargp, VarRef)) {
                AstVar* const varp = createTemp(subargp);
                // Do not remove VarRefs to this in V3Const
                varp->noSubst(true);
            }
        }
    }

    //--------------------
    // Default: Just iterate
    void visit(AstVar*) override {}  // Don't hit varrefs under vars
    void visit(AstNode* nodep) override { iterateChildren(nodep); }

public:
    // CONSTRUCTORS
    explicit PremitVisitor(AstNetlist* nodep) { iterate(nodep); }
    ~PremitVisitor() override {
        V3Stats::addStat("Optimizations, Prelim extracted value to ConstPool",
                         m_extractedToConstPool);
        V3Stats::addStat("Optimizations, Prelim temporary variables created",
                         m_temporaryVarsCreated);
    }
};

//######################################################################
// Premit class functions

void V3Premit::premitAll(AstNetlist* nodep) {
    UINFO(2, __FUNCTION__ << ":");
    { PremitVisitor{nodep}; }  // Destruct before checking
    V3Global::dumpCheckGlobalTree("premit", 0, dumpTreeEitherLevel() >= 3);
}
