// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Make lookup tables
//
// 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
//
//*************************************************************************
// TABLE TRANSFORMATIONS:
//      Look at all large always and assignments.
//      Count # of input bits and # of output bits, and # of statements
//      If high # of statements relative to inpbits*outbits,
//      replace with lookup table
//
//*************************************************************************

#include "V3PchAstNoMT.h"  // VL_MT_DISABLED_CODE_UNIT

#include "V3Table.h"

#include "V3Simulate.h"
#include "V3Stats.h"

#include <cmath>
#include <vector>

VL_DEFINE_DEBUG_FUNCTIONS;

//######################################################################
// Table class functions

// CONFIG
// 1MB is max table size (better be lots of instructs to be worth it!)
static constexpr int TABLE_MAX_BYTES = 1 * 1024 * 1024;
// 64MB is close to max memory of some systems (256MB or so), so don't get out of control
static constexpr int TABLE_TOTAL_BYTES = 64 * 1024 * 1024;
// Worth no more than 8 bytes of data to replace an instruction
static constexpr int TABLE_SPACE_TIME_MULT = 8;
// If < 32 instructions, not worth the effort
static constexpr int TABLE_MIN_NODE_COUNT = 32;
// Assume an instruction is 4 bytes
static constexpr int TABLE_BYTES_PER_INST = 4;

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

class TableVisitor;

class TableSimulateVisitor final : public SimulateVisitor {
    // MEMBERS
    TableVisitor* const m_cbthis;  ///< Class for callback

public:
    ///< Call other-this function on all new var references
    void varRefCb(AstVarRef* nodep) override;

    // CONSTRUCTORS
    explicit TableSimulateVisitor(TableVisitor* cbthis)
        : m_cbthis{cbthis} {}
    ~TableSimulateVisitor() override = default;
};

//######################################################################
// Class for holding lookup table state during construction

class TableBuilder final {
    FileLine* const m_fl;  // FileLine used during construction
    AstInitArray* m_initp = nullptr;  // The lookup table initializer values
    AstVarScope* m_varScopep = nullptr;  // The scoped variable holding the table

public:
    explicit TableBuilder(FileLine* fl)
        : m_fl{fl} {}

    ~TableBuilder() {
        if (m_initp) VL_DO_DANGLING(m_initp->deleteTree(), m_initp);
    }

    void setTableSize(AstNodeDType* elemDType, unsigned size) {
        UASSERT_OBJ(!m_initp, m_fl, "Table size already set");
        UASSERT_OBJ(size > 0, m_fl, "Size zero");
        // TODO: Assert elemDType is a packed type
        // Create data type
        const int width = elemDType->width();
        AstNodeDType* const subDTypep
            = elemDType->isString() || elemDType->isDouble()
                  ? elemDType
                  : v3Global.rootp()->findBitDType(width, width, VSigning::UNSIGNED);
        AstUnpackArrayDType* const tableDTypep = new AstUnpackArrayDType{
            m_fl, subDTypep, new AstRange{m_fl, static_cast<int>(size), 0}};
        v3Global.rootp()->typeTablep()->addTypesp(tableDTypep);
        // Create table initializer (with default value 0)
        AstConst* const defaultp
            = elemDType->isString()   ? new AstConst{m_fl, AstConst::String{}, ""}
              : elemDType->isDouble() ? new AstConst{m_fl, AstConst::RealDouble{}, 0.0}
                                      : new AstConst{m_fl, AstConst::WidthedValue{}, width, 0};
        m_initp = new AstInitArray{m_fl, tableDTypep, defaultp};
    }

    void addValue(unsigned index, const V3Number& value) {
        UASSERT_OBJ(!m_varScopep, m_fl, "Table variable already created");
        // Default value is zero/empty string so don't add it
        if (value.isString() ? value.toString().empty() : value.isEqZero()) return;
        m_initp->addIndexValuep(index, new AstConst{m_fl, value});
    }

    AstVarScope* varScopep() {
        if (!m_varScopep) m_varScopep = v3Global.rootp()->constPoolp()->findTable(m_initp);
        return m_varScopep;
    }
};

//######################################################################
// Class for holding output variable state during table conversion of logic

class TableOutputVar final {
    AstVarScope* const m_varScopep;  // The output variable
    const unsigned m_ord;  // Output ordinal number in this block
    bool m_mayBeUnassigned = false;  // If true, then this variable may be unassigned through
                                     // some path through the block being table converted
    TableBuilder m_tableBuilder;

public:
    TableOutputVar(AstVarScope* varScopep, unsigned ord)
        : m_varScopep{varScopep}
        , m_ord{ord}
        , m_tableBuilder{varScopep->fileline()} {}

    AstVarScope* varScopep() const { return m_varScopep; }
    string name() const { return varScopep()->varp()->name(); }
    unsigned ord() const { return m_ord; }
    void setMayBeUnassigned() { m_mayBeUnassigned = true; }
    bool mayBeUnassigned() const { return m_mayBeUnassigned; }
    void setTableSize(unsigned size) { m_tableBuilder.setTableSize(varScopep()->dtypep(), size); }
    void addValue(unsigned index, const V3Number& value) { m_tableBuilder.addValue(index, value); }
    AstVarScope* tabeVarScopep() { return m_tableBuilder.varScopep(); }
};

//######################################################################
// Table class functions

class TableVisitor final : public VNVisitor {
    // NODE STATE
    // Cleared on each always/assignw

    // STATE
    double m_totalBytes = 0;  // Total bytes in tables created
    VDouble0 m_statTablesCre;  // Statistic tracking

    //  State cleared on each module
    AstNodeModule* m_modp = nullptr;  // Current MODULE
    int m_modTables = 0;  // Number of tables created in this module

    //  State cleared on each scope
    AstScope* m_scopep = nullptr;  // Current SCOPE

    //  State cleared on each always/assignw
    bool m_assignDly = false;  // Consists of delayed assignments instead of normal assignments
    unsigned m_inWidthBits = 0;  // Input table width - in bits
    unsigned m_outWidthBytes = 0;  // Output table width - in bytes
    std::vector<AstVarScope*> m_inVarps;  // Input variable list
    std::vector<TableOutputVar> m_outVarps;  // Output variable list

    // METHODS

public:
    void simulateVarRefCb(AstVarRef* nodep) {
        // Called by TableSimulateVisitor on each unique varref encountered
        UINFO(9, "   SimVARREF " << nodep);
        AstVarScope* const vscp = nodep->varScopep();
        if (nodep->access().isWriteOrRW()) {
            // We'll make the table with a separate natural alignment for each output var, so
            // always have 8, 16 or 32 bit widths, so use widthTotalBytes
            m_outWidthBytes += nodep->varp()->dtypeSkipRefp()->widthTotalBytes();
            m_outVarps.emplace_back(vscp, static_cast<unsigned>(m_outVarps.size()));
        }
        if (nodep->access().isReadOrRW()) {
            m_inWidthBits += nodep->varp()->width();
            m_inVarps.push_back(vscp);
        }
    }

private:
    bool treeTest(AstAlways* nodep) {
        // Process alw/assign tree
        m_inWidthBits = 0;
        m_outWidthBytes = 0;
        m_inVarps.clear();
        m_outVarps.clear();

        // Collect stats
        TableSimulateVisitor chkvis{this};
        chkvis.mainTableCheck(nodep);
        m_assignDly = chkvis.isAssignDly();
        // Also sets m_inWidthBits
        // Also sets m_outWidthBytes
        // Also sets m_inVarps
        // Also sets m_outVarps

        // Calc data storage in bytes
        const size_t chgWidth = m_outVarps.size();
        const double space = std::pow<double>(2.0, m_inWidthBits) * (m_outWidthBytes + chgWidth);
        // Instruction count bytes (ok, it's space also not time :)
        const double time  // max(_, 1), so we won't divide by zero
            = std::max<double>(chkvis.instrCount() * TABLE_BYTES_PER_INST + chkvis.dataCount(), 1);
        if (chkvis.isImpure()) chkvis.clearOptimizable(nodep, "Table creates side effects");
        if (chkvis.isCoverage()) {
            chkvis.clearOptimizable(nodep, "Table removes coverage points");
        }
        // cppcheck-suppress knownConditionTrueFalse // set by TableSimulateVisitor
        if (!m_outWidthBytes || !m_inWidthBits) {
            chkvis.clearOptimizable(nodep, "Table has no outputs");
        }
        if (chkvis.instrCount() < TABLE_MIN_NODE_COUNT) {
            chkvis.clearOptimizable(nodep, "Table has too few nodes involved");
        }
        // cppcheck-suppress knownConditionTrueFalse
        if (space > TABLE_MAX_BYTES) {
            chkvis.clearOptimizable(nodep, "Table takes too much space");
        }
        if (space > time * TABLE_SPACE_TIME_MULT) {
            chkvis.clearOptimizable(nodep, "Table has bad tradeoff");
        }
        if (m_totalBytes > TABLE_TOTAL_BYTES) {
            chkvis.clearOptimizable(nodep, "Table out of memory");
        }
        UINFO(4, "  Test: Opt=" << (chkvis.optimizable() ? "OK" : "NO") << ", Instrs="
                                << chkvis.instrCount() << " Data=" << chkvis.dataCount()
                                << " in width (bits)=" << m_inWidthBits << " out width (bytes)="
                                << m_outWidthBytes << " Spacetime=" << (space / time) << "("
                                << space << "/" << time << ")" << ": " << nodep);
        if (chkvis.optimizable()) {
            UINFO(3, " Table Optimize spacetime=" << (space / time) << " " << nodep);
            m_totalBytes += space;
        }
        return chkvis.optimizable();
    }

    void replaceWithTable(AstAlways* nodep) {
        // We've determined this table of nodes is optimizable, do it.
        ++m_modTables;
        ++m_statTablesCre;

        FileLine* const fl = nodep->fileline();

        // We will need a table index variable, create it here.
        AstVar* const indexVarp
            = new AstVar{fl, VVarType::BLOCKTEMP, "__Vtableidx" + cvtToStr(m_modTables),
                         VFlagBitPacked{}, static_cast<int>(m_inWidthBits)};
        m_modp->addStmtsp(indexVarp);
        AstVarScope* const indexVscp = new AstVarScope{indexVarp->fileline(), m_scopep, indexVarp};
        m_scopep->addVarsp(indexVscp);

        // The 'output assigned' table builder
        TableBuilder outputAssignedTableBuilder{fl};
        outputAssignedTableBuilder.setTableSize(
            nodep->findBitDType(m_outVarps.size(), m_outVarps.size(), VSigning::UNSIGNED),
            VL_MASK_I(m_inWidthBits));

        // Set sizes of output tables
        for (TableOutputVar& tov : m_outVarps) tov.setTableSize(VL_MASK_I(m_inWidthBits));

        // Populate the tables
        createTables(nodep, outputAssignedTableBuilder);

        AstNode* const stmtsp = createLookupInput(fl, indexVscp);
        createOutputAssigns(nodep, stmtsp, indexVscp, outputAssignedTableBuilder.varScopep());

        // Link it in.
        // Keep sensitivity list, but delete all else
        nodep->stmtsp()->unlinkFrBackWithNext()->deleteTree();
        nodep->addStmtsp(stmtsp);
        UINFOTREE(6, nodep, "", "table_new");
    }

    void createTables(AstAlways* nodep, TableBuilder& outputAssignedTableBuilder) {
        // Create table
        // There may be a simulation path by which the output doesn't change value.
        // We could bail on these cases, or we can have a "change it" boolean.
        // We've chosen the latter route, since recirc is common in large FSMs.
        TableSimulateVisitor simvis{this};
        for (uint32_t i = 0; i <= VL_MASK_I(m_inWidthBits); ++i) {
            const uint32_t inValue = i;
            // Make a new simulation structure so we can set new input values
            UINFO(8, " Simulating " << std::hex << inValue);

            // Above simulateVisitor clears user 3, so
            // all outputs default to nullptr to mean 'recirculating'.
            simvis.clear();

            // Set all inputs to the constant
            uint32_t shift = 0;
            for (AstVarScope* invscp : m_inVarps) {
                // LSB is first variable, so extract it that way
                const AstConst cnst{invscp->fileline(), AstConst::WidthedValue{}, invscp->width(),
                                    VL_MASK_I(invscp->width()) & (inValue >> shift)};
                simvis.newValue(invscp, &cnst);
                shift += invscp->width();
                // We are using 32 bit arithmetic, because there's no way the input table can be
                // 2^32 bytes!
                UASSERT_OBJ(shift <= 32, nodep, "shift overflow");
                UINFO(8, "   Input " << invscp->name() << " = " << cnst.name());
            }

            // Simulate
            simvis.mainTableEmulate(nodep);
            UASSERT_OBJ(simvis.optimizable(), simvis.whyNotNodep(),
                        "Optimizable cleared, even though earlier test run said not: "
                            << simvis.whyNotMessage());

            // Build output value tables and the assigned flags table
            V3Number outputAssignedMask{nodep, static_cast<int>(m_outVarps.size()), 0};
            for (TableOutputVar& tov : m_outVarps) {
                if (V3Number* const outnump = simvis.fetchOutNumberNull(tov.varScopep())) {
                    UINFO(8, "   Output " << tov.name() << " = " << *outnump);
                    UASSERT_OBJ(!outnump->isAnyXZ(), outnump, "Table should not contain X/Z");
                    outputAssignedMask.setBit(tov.ord(), 1);  // Mark output as assigned
                    tov.addValue(inValue, *outnump);
                } else {
                    UINFO(8, "   Output " << tov.name() << " not set for this input");
                    tov.setMayBeUnassigned();
                }
            }

            // Set changed table
            outputAssignedTableBuilder.addValue(inValue, outputAssignedMask);
        }  // each value
    }

    AstNode* createLookupInput(FileLine* fl, AstVarScope* indexVscp) {
        // Concat inputs into a single temp variable (inside always)
        // First var in inVars becomes the LSB of the concat
        AstNodeExpr* concatp = nullptr;
        for (AstVarScope* invscp : m_inVarps) {
            AstVarRef* const refp = new AstVarRef{fl, invscp, VAccess::READ};
            if (concatp) {
                concatp = new AstConcat{fl, refp, concatp};
            } else {
                concatp = refp;
            }
        }

        return new AstAssign{fl, new AstVarRef{fl, indexVscp, VAccess::WRITE}, concatp};
    }

    AstArraySel* select(FileLine* fl, AstVarScope* fromp, AstVarScope* indexp) {
        AstVarRef* const fromRefp = new AstVarRef{fl, fromp, VAccess::READ};
        AstVarRef* const indexRefp = new AstVarRef{fl, indexp, VAccess::READ};
        return new AstArraySel{fl, fromRefp, indexRefp};
    }

    void createOutputAssigns(AstNode* nodep, AstNode* stmtsp, AstVarScope* indexVscp,
                             AstVarScope* outputAssignedTableVscp) {
        FileLine* const fl = nodep->fileline();
        for (TableOutputVar& tov : m_outVarps) {
            AstNodeExpr* const alhsp = new AstVarRef{fl, tov.varScopep(), VAccess::WRITE};
            AstNodeExpr* const arhsp = select(fl, tov.tabeVarScopep(), indexVscp);
            AstNode* outsetp = m_assignDly
                                   ? static_cast<AstNode*>(new AstAssignDly{fl, alhsp, arhsp})
                                   : static_cast<AstNode*>(new AstAssign{fl, alhsp, arhsp});

            // If this output is unassigned on some code paths, wrap the assignment in an If
            if (tov.mayBeUnassigned()) {
                V3Number outputChgMask{nodep, static_cast<int>(m_outVarps.size()), 0};
                outputChgMask.setBit(tov.ord(), 1);
                AstNodeExpr* const condp
                    = new AstAnd{fl, select(fl, outputAssignedTableVscp, indexVscp),
                                 new AstConst{fl, outputChgMask}};
                outsetp = new AstIf{fl, condp, outsetp};
            }

            stmtsp->addNext(outsetp);
        }
    }

    // VISITORS
    void visit(AstNode* nodep) override { iterateChildren(nodep); }
    void visit(AstNodeModule* nodep) override {
        VL_RESTORER(m_modp);
        VL_RESTORER(m_modTables);
        m_modp = nodep;
        m_modTables = 0;
        iterateChildren(nodep);
    }
    void visit(AstScope* nodep) override {
        UINFO(4, " SCOPE " << nodep);
        VL_RESTORER(m_scopep);
        m_scopep = nodep;
        iterateChildren(nodep);
    }
    void visit(AstAlways* nodep) override {
        UINFO(4, "  ALWAYS  " << nodep);
        if (treeTest(nodep)) {
            // Well, then, I'll be a memory hog.
            replaceWithTable(nodep);
        }
    }
    void visit(AstNodeAssign* nodep) override {
        // It's nearly impossible to have a large enough assign to make this worthwhile
        // For now we won't bother.
        // Accelerated: no iterate
    }

public:
    // CONSTRUCTORS
    explicit TableVisitor(AstNetlist* nodep) { iterate(nodep); }
    ~TableVisitor() override {  //
        V3Stats::addStat("Optimizations, Tables created", m_statTablesCre);
    }
};

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

void TableSimulateVisitor::varRefCb(AstVarRef* nodep) {
    // Called by checking on each new varref encountered
    // We cross-call into a TableVisitor function.
    m_cbthis->simulateVarRefCb(nodep);
}

//######################################################################
// Table class functions

void V3Table::tableAll(AstNetlist* nodep) {
    UINFO(2, __FUNCTION__ << ":");
    { TableVisitor{nodep}; }  // Destruct before checking
    V3Global::dumpCheckGlobalTree("table", 0, dumpTreeEitherLevel() >= 3);
}
