// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Waves tracing
//
// 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
//
//*************************************************************************
// V3TraceDecl's Transformations:
//      Create trace init CFunc
//      For each VarScope
//          If appropriate type of signal, create a TraceDecl
//
//*************************************************************************

#include "V3PchAstNoMT.h"  // VL_MT_DISABLED_CODE_UNIT

#include "V3TraceDecl.h"

#include "V3Ast.h"
#include "V3Control.h"
#include "V3EmitCBase.h"
#include "V3Error.h"
#include "V3File.h"
#include "V3Global.h"
#include "V3Number.h"
#include "V3Stats.h"
#include "V3UniqueNames.h"

#include <cstdint>
#include <functional>
#include <limits>
#include <tuple>
#include <unordered_map>
#include <vector>

VL_DEFINE_DEBUG_FUNCTIONS;

//######################################################################
// Utility class to emit path adjustments

class PathAdjustor final {
    FileLine* const m_flp;  // FileLine used for created nodes
    std::function<void(AstNodeStmt*)> m_emit;  // Function called with adjustment statements
    std::vector<std::string> m_stack{""};  // Stack of current paths

    static constexpr char SEPARATOR = ' ';

public:
    explicit PathAdjustor(FileLine* flp, std::function<void(AstNodeStmt*)> emit)
        : m_flp{flp}
        , m_emit{emit} {}

    // Emit Prefix adjustments until the current path is 'newPath'
    void adjust(const string& newPath, AstCell* cellp, AstVarScope* vscp) {
        // Move up to enclosing path
        while (!VString::startsWith(newPath, m_stack.back())) {
            m_emit(new AstTracePopPrefix{m_flp});
            m_stack.pop_back();
        }

        if (newPath == m_stack.back()) return;

        const VTracePrefixType lastScopeType =  //
            (cellp && VN_IS(cellp->modp(), Iface))
                    || (vscp && VN_IS(vscp->dtypep(), IfaceRefDType))
                ? VTracePrefixType::SCOPE_INTERFACE
                : VTracePrefixType::SCOPE_MODULE;
        const std::string extraPrefix = newPath.substr(m_stack.back().size());
        size_t begin = 0;
        const size_t last = extraPrefix.rfind(SEPARATOR);
        // Move down, one path element at a time
        while (true) {
            const size_t end = extraPrefix.find(SEPARATOR, begin);
            if (end == string::npos) break;
            const string& extra = extraPrefix.substr(begin, end - begin);
            if (end == last) {
                m_emit(new AstTracePushPrefix{m_flp, extra, lastScopeType});
            } else {
                m_emit(new AstTracePushPrefix{m_flp, extra, VTracePrefixType::SCOPE_MODULE});
            }
            m_stack.push_back(m_stack.back() + extra + SEPARATOR);
            begin = end + 1;
        }
        UASSERT(begin == extraPrefix.size(), "Should have consumed all of extraPrefix");
    }

    // Emit Prefix adjustments to unwind the path back to its original state
    void unwind() {
        while (m_stack.size() > 1) {
            m_emit(new AstTracePopPrefix{m_flp});
            m_stack.pop_back();
        }
    }
};

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

class TraceDeclVisitor final : public VNVisitor {
    // NODE STATE
    // AstCFunc::user1()                // code offset for current type
    // AstCFunc::user2()                // VarScope for dtype functions

    // STATE
    AstTopScope* const m_topScopep;  // The singleton AstTopScope
    const AstScope* m_currScopep = nullptr;  // Current scope being visited

    std::vector<AstCFunc*> m_topFuncps;  // Top level trace initialization functions
    std::vector<AstCFunc*> m_subFuncps;  // Trace sub functions for this scope
    std::set<const AstTraceDecl*> m_declUncalledps;  // Declarations not called
    // Functions per type and variable kind (wire vs logic etc.)
    struct DtypeFuncKey final {
        const AstNodeDType* dtypep;
        VVarType varType;
        bool operator==(const DtypeFuncKey& other) const {
            return dtypep == other.dtypep && varType == other.varType;
        }
    };
    struct DtypeFuncKeyHash final {
        size_t operator()(const DtypeFuncKey& key) const {
            return std::hash<const AstNodeDType*>{}(key.dtypep)
                   ^ (std::hash<uint8_t>{}(key.varType) << 1);
        }
    };
    std::unordered_map<DtypeFuncKey, AstCFunc*, DtypeFuncKeyHash>
        m_dtypeFuncs;  // Functions per type+kind
    AstCFunc* m_dtypeFuncp = nullptr;  // Current type func
    AstCFunc* m_dtypeSubFuncp = nullptr;  // Current type sub func
    const string m_dtypeArgs{", const char* name, uint32_t fidx, uint32_t c, "
                             "VerilatedTraceSigDirection direction"};  // Type func args
    AstTraceDecl* m_dtypeDeclp = nullptr;  // Current type func decl
    V3UniqueNames m_dtypeNames{""};  // Unique names for dtype funcs
    bool m_skipDtypeFunc = false;  // Don't create a type func
    uint32_t m_offset = std::numeric_limits<uint32_t>::max();  // Offset for types
    int m_topFuncSize = 0;  // Size of the top function currently being built
    int m_subFuncSize = 0;  // Size of the sub function currently being built
    bool m_forceNewLeaf = false;  // Force next addToSubFunc to start a fresh leaf
    size_t m_topScopeRootFuncCount = 0;  // Top-scope init functions used only for wrapper IOs
    bool m_topScopeRootPhase = false;  // Emitting top-scope wrapper IO declarations
    const int m_funcSizeLimit  // Maximum size of a function
        = v3Global.opt.outputSplitCTrace() ? v3Global.opt.outputSplitCTrace()
                                           : std::numeric_limits<int>::max();
    // Trace init functions to for each scope
    std::unordered_map<const AstScope*, std::vector<AstCFunc*>> m_scopeInitFuncps;
    // Map from hierarchical scope name to the corresponding AstScope. Note that
    // this is a many-to-one mapping for interfaces, due to interface refs.
    std::unordered_map<std::string, const AstScope*> m_pathToScopep;
    // Cell initialization placeholders:
    // (parent scope, cell under parent scope, statement)
    std::vector<std::tuple<AstScope*, AstCell*, AstNodeStmt*>> m_cellInitPlaceholders;
    // Interface refs initialization placeholders:
    // (Interface ref variable, placeholder statement)
    std::vector<std::tuple<AstVarScope*, AstNodeStmt*>> m_ifaceRefInitPlaceholders;

    // A trace entry under a scope is either:
    // - A variable (including interface references)
    // - A sub scope (stored as the cell corresponding to the sub scope)
    // Note: members are non-const to allow copy during sorting
    class TraceEntry final {
        AstVarScope* m_vscp = nullptr;  // AstVarScope under scope being traced
        AstCell* m_cellp = nullptr;  // Sub scope (as AstCell) under scope being traced
        std::string m_path;  // Path to enclosing module in original hierarchy
        std::string m_name;  // Name of signal/subscope

        void init(const std::string& name, AstNode* nodep, bool inTopScope) {
            // Compute path in hierarchy and item name
            const std::string& vcdName = AstNode::vcdName(name);
            AstVar* const varp = VN_CAST(nodep, Var);
            if (VN_IS(nodep, Cell) || VN_IS(varp->dtypep(), IfaceRefDType)) {
                // Cell or interface reference
                m_path = vcdName + " ";
                m_name.clear();
            } else if (varp->isPrimaryIO()) {
                // Primary IO variable
                m_path = "$rootio ";
                m_name = vcdName;
            } else {
                // Other Variable
                const size_t pos = vcdName.rfind(' ');
                const size_t pathLen = pos == std::string::npos ? 0 : pos + 1;
                m_path = vcdName.substr(0, pathLen);
                m_name = vcdName.substr(pathLen);
            }

            // When creating a --lib-create library, drop the name of the selected top module.
            // This will be replaced by the instance name in the model that uses the library, or
            // restored at runtime if the library itself is traced as the root model. Other top
            // level entities ($unit, packages, ...) keep a '$libroot' wrapper so they still have
            // a stable location in the dump.
            if (inTopScope && !v3Global.opt.libCreate().empty()) {
                const size_t start = m_path.find(' ');
                // Must have a prefix in the top scope with lib, as top wrapper signals not traced
                UASSERT_OBJ(start != std::string::npos, nodep, "No prefix with --lib-create");
                const std::string prefix = m_path.substr(0, start);
                // Wrapper primary IOs stay under $rootio so a root-traced library can restore
                // them under the runtime C++ instance name without affecting embedded use.
                if (prefix == "$rootio") return;
                m_path = m_path.substr(start + 1);
                if (v3Global.rootp()->traceLibTopName() != prefix) m_path = "$libroot " + m_path;
            }
        }

    public:
        explicit TraceEntry(const AstScope* scopep, AstVarScope* vscp)
            : m_vscp{vscp} {
            init(vscp->varp()->name(), vscp->varp(), scopep->isTop());
        }
        explicit TraceEntry(const AstScope* scopep, AstCell* cellp)
            : m_cellp{cellp} {
            init(cellp->name(), cellp, scopep->isTop());
        }
        int operatorCompare(const TraceEntry& b) const {
            if (const int cmp = path().compare(b.path())) return cmp < 0;
            if (const int cmp = fileline().operatorCompare(b.fileline())) return cmp < 0;
            return name() < b.name();
        }
        AstVarScope* vscp() const { return m_vscp; }
        AstCell* cellp() const { return m_cellp; }
        const std::string& path() const { return m_path; }
        void path(const std::string& path) { m_path = path; }
        const std::string& name() const { return m_name; }
        FileLine& fileline() const { return m_vscp ? *m_vscp->fileline() : *m_cellp->fileline(); }
    };
    std::vector<TraceEntry> m_entries;  // Trace entries under current scope
    AstVarScope* m_traVscp = nullptr;  // Current AstVarScope we are constructing AstTraceDecls for
    AstNodeExpr* m_traValuep = nullptr;  // Value expression for current signal
    string m_traName;  // Name component for current signal

    VDouble0 m_statSigs;  // Statistic tracking
    VDouble0 m_statIgnSigs;  // Statistic tracking

    // METHODS

    string vscIgnoreTrace(const AstVarScope* nodep) {
        // Return true if this shouldn't be traced
        // See also similar rule in V3Coverage::varIgnoreToggle
        const AstVar* const varp = nodep->varp();
        if (!varp->isTrace()) return "Verilator trace_off";
        if (!nodep->isTrace()) return "Verilator instance trace_off";
        // Automatics (typically, excluding forks) have no persistance over
        // time, and may optimize differently when multithreadeded or hierarchical.
        // Class automatics refer to being in a class but might still be pointed
        // to by a static, so are ok.
        if (varp->lifetime().isAutomatic() && !varp->isClassMember() && !varp->isParam())
            return "Automatic variable";

        const int width = recurseDTypeWidth(nodep->varp()->dtypep());
        if (v3Global.opt.traceMaxWidth() && width > v3Global.opt.traceMaxWidth())
            return "Width " + cvtToStr(width) + " > --trace-max-width";

        const string prettyName = nodep->prettyName();
        if (!V3Control::getScopeTraceOn(prettyName)) return "Vlt scope trace_off";
        if (!v3Global.opt.traceUnderscore()) {
            if (!prettyName.empty() && prettyName[0] == '_') return "Leading underscore";
            if (prettyName.find("._") != string::npos) return "Inlined leading underscore";
        }
        return ""s;
    }

    int recurseDTypeWidth(const AstNodeDType* nodep) {
        if (const AstNodeArrayDType* adtypep = VN_CAST(nodep, NodeArrayDType))
            return recurseDTypeWidth(adtypep->subDTypep()) * adtypep->declRange().elements();
        return nodep->width();
    }

    AstCFunc* newCFunc(FileLine* flp, const string& name) {
        AstScope* const topScopep = m_topScopep->scopep();
        AstCFunc* const funcp = new AstCFunc{flp, name, topScopep};
        funcp->argTypes(v3Global.opt.traceClassBase() + "* tracep");
        funcp->isTrace(true);
        funcp->isStatic(false);
        funcp->isLoose(true);
        funcp->slow(true);
        topScopep->addBlocksp(funcp);
        return funcp;
    }

    void addToTopFunc(AstNodeStmt* stmtp) {
        if (m_topFuncSize > m_funcSizeLimit || m_topFuncps.empty()) {
            m_topFuncSize = 0;
            //
            const string n = cvtToStr(m_topFuncps.size());
            const string name{"trace_init_top__" + n};
            AstCFunc* const funcp = newCFunc(m_topScopep->fileline(), name);
            m_topFuncps.push_back(funcp);
        }
        m_topFuncps.back()->addStmtsp(stmtp);
        m_topFuncSize += stmtp->nodeCount();
    }

    void addToSubFunc(AstNodeStmt* stmtp) {
        // TODO (maybe) -- sub funcs for dtype components
        if (m_dtypeSubFuncp) {
            if (m_subFuncSize > m_funcSizeLimit) newDeclSubFunc();
            m_dtypeSubFuncp->addStmtsp(stmtp);
            m_subFuncSize += stmtp->nodeCount();
            return;
        }
        // Defer trace splitting until V3Trace
        if (m_subFuncps.empty() || m_forceNewLeaf) {
            m_forceNewLeaf = false;
            FileLine* const flp = m_currScopep->fileline();
            const string n = cvtToStr(m_subFuncps.size());
            const string name
                = m_currScopep == m_topScopep->scopep() && !v3Global.opt.libCreate().empty()
                      ? (m_topScopeRootPhase ? "trace_init_leaf_root__" : "trace_init_leaf_top__")
                            + n
                      : "trace_init_sub__" + m_currScopep->nameDotless() + "__" + n;
            AstCFunc* const funcp = newCFunc(flp, name);
            funcp->addStmtsp(new AstCStmt{flp, "const int c = vlSymsp->__Vm_baseCode;"});
            m_subFuncps.push_back(funcp);
        }
        m_subFuncps.back()->addStmtsp(stmtp);
        m_subFuncSize += stmtp->nodeCount();
    }

    AstTraceDecl* addTraceDecl(const VNumRange& arrayRange,
                               int widthOverride,  // If !=0, is packed struct/array where basicp
                                                   // size misreflects one element
                               AstCCall* const dtypeCallp = nullptr) {
        VNumRange bitRange;
        if (widthOverride) {
            bitRange = VNumRange{widthOverride - 1, 0};
        } else if (const AstBasicDType* const bdtypep = m_traValuep->dtypep()->basicp()) {
            bitRange = bdtypep->nrange();
        }
        FileLine* const flp = m_traVscp->fileline();
        AstNodeExpr* valuep = m_traValuep->cloneTree(false);
        const bool validOffset = m_offset != std::numeric_limits<uint32_t>::max();
        AstTraceDecl* const newp
            = new AstTraceDecl{flp,      m_traName,  m_traVscp->varp(), valuep,
                               bitRange, arrayRange, dtypeCallp,        validOffset};
        if (validOffset) {
            newp->code(m_offset);
            if (!dtypeCallp) { m_offset += newp->codeInc(); }
            valuep->foreach([&](AstVarRef* const refp) {
                UASSERT_OBJ(refp->varScopep() == m_traVscp, refp,
                            "Trace decl expression references unexpected var");
                refp->replaceWith(new AstCExpr{flp, "__VdtypeVar", m_traVscp->width()});
                VL_DO_DANGLING(refp->deleteTree(), refp);
            });
        } else {
            newp->dtypeDeclp(m_dtypeDeclp);
        }
        m_declUncalledps.emplace(newp);
        addToSubFunc(newp);

        return newp;
    }

    void addIgnore(const string& why) {
        ++m_statIgnSigs;
        const std::string cmt = "Tracing: "s + m_traName + " // Ignored: " + why;
        if (debug() > 3 && m_traVscp) std::cout << "- " << m_traVscp->fileline() << cmt << '\n';
    }

    void fixupPlaceholder(const std::string& path, AstNodeStmt* placeholderp) {
        // Find the scope for the path. As we are working based on cell names,
        // it is possible there is no corresponding scope (e.g.: for an empty
        // module).
        const auto it = m_pathToScopep.find(AstNode::prettyName(path));
        if (it != m_pathToScopep.end()) {
            const AstScope* const scopep = it->second;
            FileLine* const flp = placeholderp->fileline();

            // Call the initialization functions for the scope
            AstNode* stmtp = nullptr;
            for (AstCFunc* const subFuncp : m_scopeInitFuncps.at(scopep)) {
                AstCCall* const callp = new AstCCall{flp, subFuncp};
                callp->dtypeSetVoid();
                callp->argTypes("tracep");
                stmtp = AstNode::addNext(stmtp, callp->makeStmt());
            }

            // Add after the placeholder
            if (stmtp) placeholderp->addNextHere(stmtp);
        }
        // Delete the placeholder
        placeholderp->unlinkFrBack();
        VL_DO_DANGLING(placeholderp->deleteTree(), placeholderp);
    }

    void fixupLibStub(const std::string& path, AstNodeStmt* placeholderp) {
        FileLine* const flp = placeholderp->fileline();

        // Call the initialization function for the library instance
        AstCStmt* const initp = new AstCStmt{flp};
        initp->add("{\n");
        initp->add("std::string __VlibName = vlSymsp->name();\n");
        initp->add("if (!__VlibName.empty()) __VlibName += '.';\n");
        initp->add("__VlibName += ");
        initp->add(new AstConst{flp, AstConst::String{}, AstNode::prettyName(path)});
        initp->add(";\n");
        initp->add("tracep->initLib(__VlibName);\n");
        initp->add("}\n");

        placeholderp->addNextHere(initp);
        // Delete the placeholder
        VL_DO_DANGLING(placeholderp->unlinkFrBack()->deleteTree(), placeholderp);
    }

    void fixupPlaceholders() {
        // Fix up cell initialization placehodlers
        UINFO(9, "fixupPlaceholders()");
        for (const auto& item : m_cellInitPlaceholders) {
            const AstScope* const parentp = std::get<0>(item);
            const AstCell* const cellp = std::get<1>(item);
            AstNodeStmt* const placeholderp = std::get<2>(item);
            const std::string path = parentp->name() + "__DOT__" + cellp->name();
            if (cellp->modp()->verilatorLib()) {
                fixupLibStub(path, placeholderp);
            } else {
                fixupPlaceholder(path, placeholderp);
            }
        }

        // Fix up interface reference initialization placeholders
        for (const auto& item : m_ifaceRefInitPlaceholders) {
            const AstVarScope* const vscp = std::get<0>(item);
            AstNodeStmt* const placeholderp = std::get<1>(item);
            const std::string path = vscp->scopep()->name() + "__DOT__" + vscp->varp()->name();
            fixupPlaceholder(path, placeholderp);
        }
    }

    void removeRedundantPrefixPushPop() {
        for (const auto& pair : m_scopeInitFuncps) {
            // cppcheck-suppress constVariablePointer
            for (AstCFunc* const funcp : pair.second) {
                AstNode* prevp = nullptr;
                AstNode* currp = funcp->stmtsp();
                while (currp) {
                    AstNode* const nextp = currp->nextp();
                    if (VN_IS(prevp, TracePushPrefix) && VN_IS(currp, TracePopPrefix)) {
                        VL_DO_DANGLING(prevp->unlinkFrBack()->deleteTree(), prevp);
                        VL_DO_DANGLING(currp->unlinkFrBack()->deleteTree(), currp);
                    }
                    if (!nextp) break;
                    prevp = nextp->backp();
                    currp = nextp;
                }
            }
        }
    }

    void checkCalls(const AstCFunc* funcp) {
        if (!v3Global.opt.debugCheck()) return;
        checkCallsRecurse(funcp);
        if (!m_declUncalledps.empty()) {  // LCOV_EXCL_START
            for (auto tracep : m_declUncalledps) UINFO(0, "-nodep " << tracep);
            (*(m_declUncalledps.begin()))->v3fatalSrc("Created TraceDecl which is never called");
        }  // LCOV_EXCL_STOP
    }
    void checkCallsRecurse(const AstCFunc* funcp) {
        funcp->foreach([this](const AstNode* nodep) {
            if (const AstTraceDecl* const declp = VN_CAST(nodep, TraceDecl)) {
                m_declUncalledps.erase(declp);
            } else if (const AstCCall* const ccallp = VN_CAST(nodep, CCall)) {
                checkCallsRecurse(ccallp->funcp());
            }
        });
    }

    bool isBasicIO() { return m_traVscp->varp()->isVLIO(); }
    void newDeclSubFunc() {
        FileLine* const flp = m_dtypeFuncp->fileline();
        AstCFunc* const subFuncp = newCFunc(flp, m_dtypeNames.get("trace_init_dtype_sub__"));
        subFuncp->argTypes(subFuncp->argTypes() + m_dtypeArgs);
        AstCCall* const subCallp = new AstCCall{flp, subFuncp};
        subCallp->dtypeSetVoid();
        subCallp->argTypes("tracep, name, fidx, c, direction");
        m_dtypeFuncp->addStmtsp(subCallp->makeStmt());
        m_dtypeSubFuncp = subFuncp;
        m_subFuncSize = 0;
    }
    void newDeclFunc(AstNodeDType* nodep) {
        AstNodeDType* const skipTypep = nodep->skipRefp();
        // offset and direction args added in EmitCImp
        std::string callArgs{"tracep, \"" + VIdProtect::protect(m_traName) + "\""};
        VL_RESTORER(m_traName);
        FileLine* const flp = skipTypep->fileline();

        const DtypeFuncKey dtypeKey{skipTypep, m_traVscp->varp()->varType()};
        auto pair = m_dtypeFuncs.emplace(dtypeKey, nullptr);
        AstCFunc** funcpp = &pair.first->second;
        if (pair.second) {
            *funcpp = newCFunc(flp, m_dtypeNames.get("trace_init_dtype__"));
            (*funcpp)->argTypes((*funcpp)->argTypes() + m_dtypeArgs);
            (*funcpp)->user2p(m_traVscp);
        }

        AstCCall* const callp = new AstCCall{flp, *funcpp};
        callp->dtypeSetVoid();
        callp->argTypes(callArgs);
        m_dtypeDeclp = addTraceDecl(VNumRange{}, skipTypep->width(), callp);
        addToSubFunc(callp->makeStmt());

        if (pair.second) {
            VL_RESTORER(m_offset);
            m_offset = 0;

            VL_RESTORER(m_dtypeFuncp);
            VL_RESTORER(m_dtypeSubFuncp);
            VL_RESTORER(m_subFuncSize);
            m_dtypeFuncp = *funcpp;
            newDeclSubFunc();
            if (AstStructDType* const dtypep = VN_CAST(skipTypep, StructDType)) {
                declStruct(dtypep, true);
            } else if (AstUnpackArrayDType* const dtypep = VN_CAST(skipTypep, UnpackArrayDType)) {
                declUnpackedArray(dtypep, true);
            } else if (AstPackArrayDType* const dtypep = VN_CAST(skipTypep, PackArrayDType)) {
                declPackedArray(dtypep, true);
            } else {
                UASSERT_OBJ(false, skipTypep, "Creating a trace function for an unexpected type");
            }
            m_dtypeFuncp->user1(m_offset);
        }

        m_dtypeDeclp->codeInc((*funcpp)->user1());
        m_offset += m_dtypeDeclp->codeInc();
    }
    void declUnpackedArray(AstUnpackArrayDType* const nodep, bool newFunc) {
        string prefixName(newFunc ? "name" : m_traName);

        VL_RESTORER(m_traName);
        FileLine* const flp = nodep->fileline();

        addToSubFunc(new AstTracePushPrefix{flp, prefixName, VTracePrefixType::ARRAY_UNPACKED,
                                            nodep->left(), nodep->right(), !newFunc});

        if (VN_IS(nodep->subDTypep()->skipRefToEnump(),
                  BasicDType)  // Nothing lower than this array
            && m_traVscp->dtypep()->skipRefToEnump() == nodep) {  // Nothing above this array
            // Simple 1-D array, use existing V3EmitC runtime loop rather than unrolling
            // This will put "(index)" at end of signal name for us
            if (m_traVscp->dtypep()->skipRefToEnump()->isString()) {
                addIgnore("Unsupported: strings");
            } else {
                m_traName = "";
                addTraceDecl(nodep->declRange(), 0);
            }
        } else {
            AstNodeDType* const subtypep = nodep->subDTypep()->skipRefToEnump();
            // Always iterate left index to right index
            const int inc = nodep->rangep()->ascending() ? 1 : -1;
            for (int i = nodep->left(); i != nodep->right() + inc; i += inc) {
                VL_RESTORER(m_traValuep);
                m_traName = '[' + std::to_string(i) + ']';
                m_traValuep = m_traValuep->cloneTree(false);
                m_traValuep = new AstArraySel{flp, m_traValuep, i - nodep->lo()};
                m_traValuep->dtypep(subtypep);
                iterate(subtypep);
                VL_DO_DANGLING(m_traValuep->deleteTree(), m_traValuep);
            }
        }

        addToSubFunc(new AstTracePopPrefix{flp});
    }
    void declPackedArray(AstPackArrayDType* const nodep, bool newFunc) {
        string prefixName(newFunc ? "name" : m_traName);
        AstNodeDType* const subtypep = nodep->subDTypep()->skipRefToEnump();

        VL_RESTORER(m_traName);
        FileLine* const flp = nodep->fileline();

        addToSubFunc(new AstTracePushPrefix{flp, prefixName, VTracePrefixType::ARRAY_PACKED,
                                            nodep->left(), nodep->right(), !newFunc});

        // Always iterate left index to right index
        const int inc = nodep->rangep()->ascending() ? 1 : -1;
        for (int i = nodep->left(); i != nodep->right() + inc; i += inc) {
            VL_RESTORER(m_traValuep);
            m_traName = '[' + std::to_string(i) + ']';
            const int lsb = (i - nodep->lo()) * subtypep->width();
            m_traValuep = m_traValuep->cloneTree(false);
            m_traValuep = new AstSel{flp, m_traValuep, lsb, subtypep->width()};
            m_traValuep->dtypep(subtypep);
            iterate(subtypep);
            VL_DO_CLEAR(m_traValuep->deleteTree(), m_traValuep = nullptr);
        }

        addToSubFunc(new AstTracePopPrefix{flp});
    }
    void declStruct(AstStructDType* const nodep, bool newFunc) {
        FileLine* const flp = nodep->fileline();
        string prefixName(newFunc ? "name" : m_traName);
        int nMembers = 0;
        for (AstNode* mp = nodep->membersp(); mp; mp = mp->nextp()) ++nMembers;
        if (!nodep->packed()) {
            addToSubFunc(new AstTracePushPrefix{flp, prefixName,  //
                                                VTracePrefixType::STRUCT_UNPACKED, nMembers, 0,
                                                !newFunc});
            for (const AstMemberDType *itemp = nodep->membersp(), *nextp; itemp; itemp = nextp) {
                nextp = VN_AS(itemp->nextp(), MemberDType);
                AstNodeDType* const subtypep = itemp->subDTypep()->skipRefToEnump();
                m_traName = itemp->prettyName();
                VL_RESTORER(m_traValuep);
                m_traValuep = m_traValuep->cloneTree(false);
                m_traValuep = new AstStructSel{flp, m_traValuep, itemp->name()};
                m_traValuep->dtypep(subtypep);
                iterate(subtypep);
                VL_DO_DANGLING(m_traValuep->deleteTree(), m_traValuep);
            }
            addToSubFunc(new AstTracePopPrefix{flp});
        } else {
            addToSubFunc(new AstTracePushPrefix{flp, prefixName,  //
                                                VTracePrefixType::STRUCT_PACKED, nMembers, 0,
                                                !newFunc});
            for (const AstMemberDType *itemp = nodep->membersp(), *nextp; itemp; itemp = nextp) {
                nextp = VN_AS(itemp->nextp(), MemberDType);
                AstNodeDType* const subtypep = itemp->subDTypep()->skipRefToEnump();
                m_traName = itemp->prettyName();
                VL_RESTORER(m_traValuep);
                m_traValuep = m_traValuep->cloneTree(false);
                m_traValuep = new AstSel{flp, m_traValuep, itemp->lsb(), subtypep->width()};
                m_traValuep->dtypep(subtypep);
                iterate(subtypep);
                VL_DO_DANGLING(m_traValuep->deleteTree(), m_traValuep);
            }
            addToSubFunc(new AstTracePopPrefix{flp});
        }
    }

    // VISITORS
    void visit(AstScope* nodep) override {
        UINFO(9, "visit " << nodep);
        UASSERT_OBJ(!m_currScopep, nodep, "Should not nest");
        UASSERT_OBJ(m_subFuncps.empty(), nodep, "Should not nest");
        UASSERT_OBJ(m_entries.empty(), nodep, "Should not nest");
        UASSERT_OBJ(!m_traVscp, nodep, "Should not nest");
        UASSERT_OBJ(!m_traValuep, nodep, "Should not nest");
        UASSERT_OBJ(m_traName.empty(), nodep, "Should not nest");

        // If this is a stub for a --lib-create library, skip.
        if (nodep->modp()->verilatorLib()) return;

        VL_RESTORER(m_currScopep);
        m_currScopep = nodep;

        // Gather signals under this scope
        iterateChildrenConst(nodep);

        // Gather cells under this scope
        for (AstNode* stmtp = nodep->modp()->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
            if (AstCell* const cellp = VN_CAST(stmtp, Cell)) m_entries.emplace_back(nodep, cellp);
        }

        if (!m_entries.empty()) {
            // Sort trace entries, by enclosing instance (necessary for single traversal of
            // hierarchy during initialization), then by source location, then by name.
            std::stable_sort(
                m_entries.begin(), m_entries.end(),
                [](const TraceEntry& a, const TraceEntry& b) { return a.operatorCompare(b); });

            FileLine* const flp = nodep->fileline();
            PathAdjustor pathAdjustor{flp, [&](AstNodeStmt* stmtp) { addToSubFunc(stmtp); }};
            const bool splitRootPrimaryIos = nodep->isTop() && !v3Global.opt.libCreate().empty();
            const auto emitEntry = [&](const TraceEntry& entry) {
                AstVarScope* const vscp = entry.vscp();
                // Adjust name prefix based on path in hierarchy
                UINFO(9, "path='" << entry.path() << "' name='" << entry.name() << "' "
                                  << (entry.cellp() ? static_cast<AstNode*>(entry.cellp())
                                                    : static_cast<AstNode*>(entry.vscp())));
                pathAdjustor.adjust(entry.path(), entry.cellp(), entry.vscp());

                m_traName = entry.name();

                if (vscp) {
                    // This is a signal: build AstTraceDecl for it
                    m_traVscp = vscp;
                    const string& ignoreReason = vscIgnoreTrace(m_traVscp);
                    if (!ignoreReason.empty()) {
                        addIgnore(ignoreReason);
                    } else {
                        ++m_statSigs;
                        // Create reference to whole signal. We will operate on this during the
                        // traversal.
                        m_traValuep
                            = new AstVarRef{m_traVscp->fileline(), m_traVscp, VAccess::READ};
                        // Recurse into data type of the signal. The visit methods will add
                        // AstTraceDecls.
                        iterate(m_traVscp->varp()->dtypep()->skipRefToEnump());
                        // Delete reference created above. Traversal cloned it as required.
                        if (m_traValuep) {
                            VL_DO_DANGLING(m_traValuep->deleteTree(), m_traValuep);
                            // Note: Sometimes VL_DANGLING is a no-op, but we have assertions
                            // on m_traValuep being nullptr, so make sure it is.
                            m_traValuep = nullptr;
                        }
                    }
                } else {
                    // This is a subscope: insert a placeholder to be fixed up later
                    AstCell* const cellp = entry.cellp();
                    AstNodeStmt* const stmtp = new AstComment{
                        cellp->fileline(), "Instance init for: " + cellp->prettyName()};
                    addToSubFunc(stmtp);
                    m_cellInitPlaceholders.emplace_back(nodep, cellp, stmtp);
                }
            };
            if (splitRootPrimaryIos) {
                m_topScopeRootPhase = true;
                for (const TraceEntry& entry : m_entries) {
                    AstVarScope* const vscp = entry.vscp();
                    if (!(vscp && vscp->varp()->isPrimaryIO())) continue;
                    emitEntry(entry);
                }
                m_topScopeRootPhase = false;
                pathAdjustor.unwind();
                m_topScopeRootFuncCount = m_subFuncps.size();
                if (m_topScopeRootFuncCount) {
                    // Force the next addToSubFunc to start a fresh leaf so
                    // root-phase decls and top-phase decls live in separate
                    // functions. Without this, both phases land in the same
                    // leaf and `trace_init_top` ends up emitting both the
                    // $rootio-prefixed IOs and the regular-scope decls, so
                    // each IO gets traced twice when the lib is consumed
                    // via initLib(name).
                    m_subFuncSize = 0;
                    m_forceNewLeaf = true;
                }
            }
            for (const TraceEntry& entry : m_entries) {
                AstVarScope* const vscp = entry.vscp();
                if (splitRootPrimaryIos && vscp && vscp->varp()->isPrimaryIO()) continue;
                emitEntry(entry);
            }
            pathAdjustor.unwind();
            m_traVscp = nullptr;
            m_traName.clear();
            UASSERT_OBJ(!m_traValuep, nodep, "Should have been deleted");
            m_entries.clear();
        }

        // Save the initialization functions of this scope
        m_scopeInitFuncps.emplace(nodep, std::move(m_subFuncps));

        // Save the hierarchical name of this scope
        const std::string path = nodep->prettyName();
        m_pathToScopep.emplace(path, nodep);

        // Save the hierarchical names of interface references that reference this scope
        const AstCell* const cellp = nodep->aboveCellp();
        if (cellp
            && VN_IS(cellp->modp(), Iface)
            // Exclude classes that were inside interfaces, the cell's modp is an interface,
            // but the nodep->module is the Class
            && VN_IS(nodep->modp(), Iface)) {
            const size_t lastDot = path.find_last_of('.');
            UASSERT_OBJ(lastDot != string::npos, nodep,
                        "Expected an interface scope name to have at least one dot: " << path);
            const std::string parentPath = path.substr(0, lastDot + 1);

            for (AstIntfRef *intfRefp = cellp->intfRefsp(), *nextp; intfRefp; intfRefp = nextp) {
                nextp = VN_AS(intfRefp->nextp(), IntfRef);

                const std::string refName = intfRefp->prettyName();

                // Assume only references under the same parent scope reference
                // the same interface.
                // TODO: This is not actually correct. An interface can propagate
                //       upwards and sideways when passed to a port via a downward
                //       hierarchical reference, which we will miss here.
                if (!VString::startsWith(refName, parentPath)) continue;

                // Save the mapping from the path of the reference to the scope
                m_pathToScopep.emplace(refName, nodep);

                // No more need for AstIntfRef
                intfRefp->unlinkFrBack();
                VL_DO_DANGLING(intfRefp->deleteTree(), intfRefp);
            }
        }
    }
    void visit(AstVarScope* nodep) override {
        UASSERT_OBJ(m_currScopep, nodep, "AstVarScope not under AstScope");

        // Prefilter - things that get added to m_vscps will either get traced or get a comment as
        // to why they are not traced. Generally these conditions doesn't need updating, instead
        // use varp->isTrace() and/or vscIgnoreTrace.
        if (nodep->varp()->isTemp() && !nodep->varp()->isTrace()) return;
        if (nodep->varp()->isClassMember()) return;
        if (nodep->varp()->isFuncLocal()) return;

        // When creating a --lib-create library ...
        if (!v3Global.opt.libCreate().empty()) {
            // Ignore parameters in packages. These will be traced at the top level.
            if (nodep->varp()->isParam() && VN_IS(nodep->scopep()->modp(), Package)) return;
        }

        // Add to traced signal list
        m_entries.emplace_back(m_currScopep, nodep);
    }

    // VISITORS - Data types when tracing
    void visit(AstConstDType* nodep) override {
        if (!m_traVscp) return;
        VL_RESTORER(m_offset);
        VL_RESTORER(m_skipDtypeFunc);
        m_skipDtypeFunc = true;
        iterate(nodep->subDTypep()->skipRefToEnump());
    }
    void visit(AstRefDType* nodep) override {
        if (!m_traVscp) return;
        VL_RESTORER(m_offset);
        iterate(nodep->subDTypep()->skipRefToEnump());
    }
    void visit(AstIfaceRefDType* /*nodep*/) override {
        if (!m_traVscp) return;
        // Insert a placeholder to be fixed up later
        FileLine* const flp = m_traVscp->fileline();
        AstNodeStmt* const stmtp
            = new AstComment{flp, "Interface ref init for: " + m_traVscp->prettyName()};
        addToSubFunc(stmtp);
        m_ifaceRefInitPlaceholders.emplace_back(m_traVscp, stmtp);
    }
    void visit(AstUnpackArrayDType* nodep) override {
        // Note more specific dtypes above
        if (!m_traVscp) return;

        if (v3Global.opt.traceMaxArray()
            && static_cast<int>(nodep->arrayUnpackedElements()) > v3Global.opt.traceMaxArray()) {
            addIgnore("Wide memory > --trace-max-array ents");
            return;
        }

        // Skip unpacked arrays of non-traceable leaf types (e.g. strings)
        if (VN_IS(nodep->subDTypep()->skipRefToEnump(), BasicDType)
            && nodep->subDTypep()->skipRefToEnump()->isString()) {
            addIgnore("Unsupported: strings");
            return;
        }

        VL_RESTORER(m_skipDtypeFunc);
        VL_RESTORER(m_dtypeDeclp);
        if (isBasicIO()) m_skipDtypeFunc = true;

        if (!(m_skipDtypeFunc || m_dtypeDeclp)) {
            VL_RESTORER(m_offset);
            newDeclFunc(nodep);
        }
        declUnpackedArray(nodep, false);
    }
    void visit(AstPackArrayDType* nodep) override {
        if (!m_traVscp) return;

        if (!v3Global.opt.traceStructs()) {
            // Everything downstream is packed, so deal with as one trace unit.
            // This may not be the nicest for user presentation, but is
            // a much faster way to trace
            addTraceDecl(VNumRange{}, nodep->width());
            return;
        }

        AstNodeDType* const subtypep = nodep->subDTypep()->skipRefToEnump();

        // Do not unroll if the elements are simple 'bit', or 'logic'
        if (AstBasicDType* const basicp = VN_CAST(subtypep->skipRefp(), BasicDType)) {
            if (basicp->isBitLogic() && !basicp->isRanged()) {
                addTraceDecl(VNumRange{}, nodep->width());
                return;
            }
        }

        VL_RESTORER(m_skipDtypeFunc);
        VL_RESTORER(m_dtypeDeclp);
        if (isBasicIO()) m_skipDtypeFunc = true;

        if (!(m_skipDtypeFunc || m_dtypeDeclp)) {
            VL_RESTORER(m_offset);
            newDeclFunc(nodep);
        }
        declPackedArray(nodep, false);
    }
    void visit(AstStructDType* nodep) override {
        if (!m_traVscp) return;

        if (nodep->packed() && !v3Global.opt.traceStructs()) {
            // Everything downstream is packed, so deal with as one trace unit
            // This may not be the nicest for user presentation, but is
            // a much faster way to trace
            addTraceDecl(VNumRange{}, nodep->width());
            return;
        }

        VL_RESTORER(m_skipDtypeFunc);
        VL_RESTORER(m_dtypeDeclp);
        if (isBasicIO()) m_skipDtypeFunc = true;

        // Only create sub functions for top-level structs, i.e. don't have struct funcs
        // call other struct funcs for child types.  This could easily be done for decl funcs
        // but full / chg funcs would require copying / aligning data for child types or more
        // complicated / wonky / generalized data access.
        if (!(m_skipDtypeFunc || m_dtypeDeclp)) {
            VL_RESTORER(m_offset);
            newDeclFunc(nodep);
        }
        declStruct(nodep, false);
    }
    void visit(AstUnionDType* nodep) override {
        if (!m_traVscp) return;

        VL_RESTORER(m_skipDtypeFunc);
        m_skipDtypeFunc = true;

        if (nodep->packed() && !v3Global.opt.traceStructs()) {
            // Everything downstream is packed, so deal with as one trace unit
            // This may not be the nicest for user presentation, but is
            // a much faster way to trace
            addTraceDecl(VNumRange{}, nodep->width());
            return;
        }

        VL_RESTORER(m_traName);
        FileLine* const flp = nodep->fileline();

        int nMembers = 0;
        for (AstNode* mp = nodep->membersp(); mp; mp = mp->nextp()) ++nMembers;

        if (!nodep->packed()) {
            addIgnore("Unsupported: Unpacked union");
        } else {
            addToSubFunc(new AstTracePushPrefix{flp, m_traName,  //
                                                VTracePrefixType::UNION_PACKED, nMembers});
            for (const AstMemberDType *itemp = nodep->membersp(), *nextp; itemp; itemp = nextp) {
                nextp = VN_AS(itemp->nextp(), MemberDType);
                AstNodeDType* const subtypep = itemp->subDTypep()->skipRefToEnump();
                m_traName = itemp->prettyName();
                iterate(subtypep);
            }
            addToSubFunc(new AstTracePopPrefix{flp});
        }
    }
    void visit(AstBasicDType* nodep) override {
        if (!m_traVscp) return;
        if (nodep->isString()) {
            addIgnore("Unsupported: strings");
        } else {
            addTraceDecl(VNumRange{}, 0);
        }
    }
    void visit(AstEnumDType* nodep) override { iterate(nodep->skipRefp()); }
    void visit(AstNodeDType*) override {
        // Note more specific dtypes above
        if (!m_traVscp) return;
        addIgnore("Unsupported: data type");
    }

    //--------------------
    void visit(AstNode* nodep) override { iterateChildren(nodep); }

public:
    // CONSTRUCTORS
    explicit TraceDeclVisitor(AstNetlist* nodep)
        : m_topScopep{nodep->topScopep()} {
        FileLine* const flp = nodep->fileline();

        // Iterate modules to build per scope initialization functions
        iterateAndNextConstNull(nodep->modulesp());
        UASSERT_OBJ(m_subFuncps.empty(), nodep, "Should have been emptied");

        // Fix up the placeholders in the initialization functions
        fixupPlaceholders();

        // Now that we have everything ready, remove redundant pushPrefix/popPrefix
        // pairs. While functionally this is not really necessary (the trace files
        // might have some empty scope declarations), we do it to preserve previous
        // behavior. Note: unfortunately generating these without the redundant
        // push/pop pairs is a bit hard. It is cleaner to remove them.
        removeRedundantPrefixPushPop();

        const std::vector<AstCFunc*>& topScopeFuncps = m_scopeInitFuncps.at(m_topScopep->scopep());
        AstCFunc* rootFuncp = nullptr;
        if (!v3Global.opt.libCreate().empty()) {
            rootFuncp = newCFunc(flp, "trace_init_root");
            rootFuncp->entryPoint(true);
            for (size_t i = 0; i < m_topScopeRootFuncCount; ++i) {
                AstCCall* const callp = new AstCCall{flp, topScopeFuncps.at(i)};
                callp->dtypeSetVoid();
                callp->argTypes("tracep");
                rootFuncp->addStmtsp(callp->makeStmt());
            }
            if (!m_topScopeRootFuncCount) rootFuncp->addStmtsp(new AstComment{flp, "Empty"});
        }

        // Call the non-wrapper initialization functions of the root scope from the top function
        for (size_t i = m_topScopeRootFuncCount; i < topScopeFuncps.size(); ++i) {
            AstCFunc* const funcp = topScopeFuncps.at(i);
            AstCCall* const callp = new AstCCall{flp, funcp};
            callp->dtypeSetVoid();
            callp->argTypes("tracep");
            addToTopFunc(callp->makeStmt());
        }

        // Ensure a top function exists, in case there was nothing to trace at all
        if (m_topFuncps.empty()) addToTopFunc(new AstComment{flp, "Empty"});

        // Create single top level function, if more than one exists
        if (m_topFuncps.size() > 1) {
            AstCFunc* const topFuncp = newCFunc(flp, "");
            for (AstCFunc* funcp : m_topFuncps) {
                AstCCall* const callp = new AstCCall{flp, funcp};
                callp->dtypeSetVoid();
                callp->argTypes("tracep");
                topFuncp->addStmtsp(callp->makeStmt());
            }
            m_topFuncps.clear();
            m_topFuncps.push_back(topFuncp);
        }

        // Set name of top level function
        AstCFunc* const topFuncp = m_topFuncps.front();
        topFuncp->name("trace_init_top");
        topFuncp->entryPoint(true);

        if (rootFuncp && v3Global.opt.debugCheck()) checkCallsRecurse(rootFuncp);
        checkCalls(topFuncp);
    }
    ~TraceDeclVisitor() override {
        V3Stats::addStat("Tracing, Traced signals", m_statSigs);
        V3Stats::addStat("Tracing, Ignored signals", m_statIgnSigs);
    }
};

//######################################################################
// Trace class functions

void V3TraceDecl::traceDeclAll(AstNetlist* nodep) {
    UINFO(2, __FUNCTION__ << ":");
    { TraceDeclVisitor{nodep}; }  // Destruct before checking
    V3Global::dumpCheckGlobalTree("tracedecl", 0, dumpTreeEitherLevel() >= 3);
}
