// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Add temporaries, such as for task 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
//
//*************************************************************************
// V3Task's Transformations:
//
// Each module:
//      Look for TASKREF
//          Insert task's statements into the referrer
//      Look for TASKs
//          Remove them, they're inlined
//
//*************************************************************************

#include "V3PchAstNoMT.h"  // VL_MT_DISABLED_CODE_UNIT

#include "V3Task.h"

#include "V3Const.h"
#include "V3Control.h"
#include "V3EmitCBase.h"
#include "V3Graph.h"
#include "V3Stats.h"
#include "V3UniqueNames.h"

#include <tuple>

VL_DEFINE_DEBUG_FUNCTIONS;

//######################################################################
// Graph subclasses

class TaskBaseVertex VL_NOT_FINAL : public V3GraphVertex {
    VL_RTTI_IMPL(TaskBaseVertex, V3GraphVertex);
    AstNode* m_impurep = nullptr;  // Node causing impure function w/ outside references
    bool m_noInline = false;  // Marked with pragma
public:
    explicit TaskBaseVertex(V3Graph* graphp)
        : V3GraphVertex{graphp} {}
    ~TaskBaseVertex() override = default;
    bool pure() const { return m_impurep == nullptr; }
    AstNode* impureNode() const { return m_impurep; }
    void impure(AstNode* nodep) { m_impurep = nodep; }
    bool noInline() const { return m_noInline; }
    void noInline(bool flag) { m_noInline = flag; }
};

class TaskFTaskVertex final : public TaskBaseVertex {
    VL_RTTI_IMPL(TaskFTaskVertex, TaskBaseVertex);
    // Every task gets a vertex, and we link tasks together based on funcrefs.
    AstNodeFTask* const m_nodep;
    AstCFunc* m_cFuncp = nullptr;
    bool m_needsNonInlineCFunc = false;  // Needs CFunc for selected non-inlined call sites

public:
    TaskFTaskVertex(V3Graph* graphp, AstNodeFTask* nodep)
        : TaskBaseVertex{graphp}
        , m_nodep{nodep} {}
    ~TaskFTaskVertex() override = default;
    AstNodeFTask* nodep() const VL_MT_STABLE { return m_nodep; }
    string name() const override VL_MT_STABLE { return nodep()->name(); }
    string dotColor() const override { return pure() ? "black" : "red"; }
    AstCFunc* cFuncp() const { return m_cFuncp; }
    void cFuncp(AstCFunc* nodep) { m_cFuncp = nodep; }
    bool needsNonInlineCFunc() const { return m_needsNonInlineCFunc; }
    void needsNonInlineCFunc(bool flag) { m_needsNonInlineCFunc = flag; }
};

class TaskCodeVertex final : public TaskBaseVertex {
    VL_RTTI_IMPL(TaskCodeVertex, TaskBaseVertex);
    // Top vertex for all calls not under another task
public:
    explicit TaskCodeVertex(V3Graph* graphp)
        : TaskBaseVertex{graphp} {}
    ~TaskCodeVertex() override = default;
    string name() const override VL_MT_STABLE { return "*CODE*"; }
    string dotColor() const override { return "green"; }
};

class TaskEdge final : public V3GraphEdge {
    VL_RTTI_IMPL(TaskEdge, V3GraphEdge)
public:
    TaskEdge(V3Graph* graphp, TaskBaseVertex* fromp, TaskBaseVertex* top)
        : V3GraphEdge{graphp, fromp, top, 1, false} {}
    ~TaskEdge() override = default;
    string dotLabel() const override { return "w" + cvtToStr(weight()); }
};

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

class TaskStateVisitor final : public VNVisitor {
    // NODE STATE
    //  Output:
    //   AstNodeFTask::user3p   // AstScope* this FTask is under
    //   AstNodeFTask::user4p   // GraphFTaskVertex* this FTask is under
    //   AstVar::user4p         // GraphFTaskVertex* this variable is declared in
    const VNUser3InUse m_inuser3;
    const VNUser4InUse m_inuser4;

    // TYPES
    using VarToScopeMap = std::map<std::pair<AstScope*, AstVar*>, AstVarScope*>;
    using FuncToClassMap = std::unordered_map<const AstNodeFTask*, AstClass*>;
    // MEMBERS
    VarToScopeMap m_varToScopeMap;  // Map for Var -> VarScope mappings
    FuncToClassMap m_funcToClassMap;  // Map for ctor func -> class
    AstNodeFTask* m_ctorp = nullptr;  // Class constructor
    AstClass* m_classp = nullptr;  // Current class
    V3Graph m_callGraph;  // Task call graph
    TaskBaseVertex* m_curVxp;  // Current vertex we're adding to
    std::vector<AstInitialAutomatic*> m_initialps;  // Initial blocks to move

public:
    // METHODS
    static bool isVirtualIfaceMethodCall(const AstNodeFTaskRef* refp) {
        const AstMethodCall* const mrefp = VN_CAST(refp, MethodCall);
        if (!mrefp) return false;
        const AstNodeExpr* const fromp = mrefp->fromp();
        if (!fromp || !fromp->dtypep()) return false;
        const AstIfaceRefDType* const ifrefp = VN_CAST(fromp->dtypep()->skipRefp(), IfaceRefDType);
        return ifrefp && ifrefp->isVirtual();
    }
    static bool isIfaceFTaskScope(const AstScope* scopep) {
        return scopep && VN_IS(scopep->modp(), Iface);
    }
    AstScope* getScope(AstNodeFTask* nodep) {
        AstScope* const scopep = VN_AS(nodep->user3p(), Scope);
        UASSERT_OBJ(scopep, nodep, "No scope for function");
        return scopep;
    }
    AstVarScope* findVarScope(AstScope* scopep, AstVar* nodep) {
        const auto iter = m_varToScopeMap.find(std::make_pair(scopep, nodep));
        UASSERT_OBJ(iter != m_varToScopeMap.end(), nodep, "No scope for var");
        return iter->second;
    }
    AstClass* getClassp(AstNodeFTask* nodep) {
        AstClass* const classp = m_funcToClassMap[nodep];
        UASSERT_OBJ(classp, nodep, "No class for ctor func");
        return classp;
    }
    void remapFuncClassp(AstNodeFTask* nodep, AstNodeFTask* newp) {
        m_funcToClassMap[newp] = getClassp(nodep);
    }
    bool ftaskNoInline(AstNodeFTask* nodep) { return getFTaskVertex(nodep)->noInline(); }
    bool ftaskNeedsNonInlineCFunc(AstNodeFTask* nodep) {
        return getFTaskVertex(nodep)->needsNonInlineCFunc();
    }
    AstCFunc* ftaskCFuncp(AstNodeFTask* nodep) { return getFTaskVertex(nodep)->cFuncp(); }
    void ftaskCFuncp(AstNodeFTask* nodep, AstCFunc* cfuncp) {
        getFTaskVertex(nodep)->cFuncp(cfuncp);
    }
    // Returns pointer to node that makes the given FTask impure
    AstNode* checkImpure(AstNodeFTask* nodep) { return getFTaskVertex(nodep)->impureNode(); }

private:
    TaskFTaskVertex* getFTaskVertex(AstNodeFTask* nodep) {
        if (!nodep->user4p()) nodep->user4p(new TaskFTaskVertex{&m_callGraph, nodep});
        return static_cast<TaskFTaskVertex*>(nodep->user4u().toGraphVertex());
    }

    void propagateImpureReason() {
        std::vector<TaskFTaskVertex*> workList;
        // Gather all impure FTask vertices
        for (V3GraphVertex& vtx : m_callGraph.vertices()) {
            if (TaskFTaskVertex* const ftVtxp = vtx.cast<TaskFTaskVertex>()) {
                if (!ftVtxp->pure()) workList.emplace_back(ftVtxp);
            }
        }
        // Mark every pure caller as impure and enqueue them - this propagates impureness
        while (!workList.empty()) {
            TaskFTaskVertex* const calleep = workList.back();
            workList.pop_back();
            UASSERT_OBJ(!calleep->pure(), calleep->nodep(), "Should not enqueue pure vertex");
            for (V3GraphEdge& edge : calleep->inEdges()) {
                if (TaskFTaskVertex* const callerp = edge.fromp()->cast<TaskFTaskVertex>()) {
                    if (!callerp->pure()) continue;  // Already processed or on workList
                    callerp->impure(calleep->impureNode());
                    workList.emplace_back(callerp);
                }
            }
        }
    }

    void decideInlining() {
        for (V3GraphVertex& vtx : m_callGraph.vertices()) {
            TaskFTaskVertex* const ftVtxp = vtx.cast<TaskFTaskVertex>();
            if (!ftVtxp) continue;
            // Move on if already decided not to inline
            if (ftVtxp->noInline()) continue;
            // If inlining blanked disabled, don't do it. Note this usually
            // results in large amounts of breakage so is mostly for testing.
            if (!v3Global.opt.fInlineFuncs()) {
                ftVtxp->noInline(true);
                continue;
            }

            // If eagerly inlining (historic - and still default - behaviour) then nothing to do
            if (v3Global.opt.fInlineFuncsEager()) continue;

            AstNodeFTask* const ftaskp = ftVtxp->nodep();
            // We are relying on inlining of some internal functions for
            // functional corretness. Probably that's not a good thing?
            if (!ftaskp->verilogFunction()) continue;
            // Non-inlned public functions are a bit broken right now, so keep
            // inlining them (internal calls are inlined + keep public version)
            if (ftaskp->taskPublic()) continue;
            // If the function is pure, it's OK not to inline, so don't do it
            if (ftVtxp->pure()) ftVtxp->noInline(true);
        }
    }

    // VISITORS
    void visit(AstScope* nodep) override {
        // Each FTask is unique per-scope, so AstNodeFTaskRefs do not need
        // pointers to what scope the FTask is to be invoked under.
        // However, to create variables, we need to track the scopes involved.
        // Find all var->varscope mappings, for later cleanup
        for (AstNode* stmtp = nodep->varsp(); stmtp; stmtp = stmtp->nextp()) {
            if (AstVarScope* const vscp = VN_CAST(stmtp, VarScope)) {
                AstVar* const varp = vscp->varp();
                if (varp->isFuncLocal() || varp->isUsedLoopIdx()
                    || varp->lifetime().isAutomatic()) {
                    UINFO(9, "   funcvsc " << vscp);
                    m_varToScopeMap.emplace(std::make_pair(nodep, varp), vscp);
                }
            }
        }
        // Likewise, all FTask->scope mappings
        for (AstNode* stmtp = nodep->blocksp(); stmtp; stmtp = stmtp->nextp()) {
            if (AstNodeFTask* const taskp = VN_CAST(stmtp, NodeFTask)) taskp->user3p(nodep);
        }
        iterateChildren(nodep);
    }
    void visit(AstNodeFTaskRef* nodep) override {
        // We make multiple edges if a task is called multiple times from another task.
        UASSERT_OBJ(nodep->taskp(), nodep, "Unlinked task");
        TaskFTaskVertex* const taskVtxp = getFTaskVertex(nodep->taskp());
        new TaskEdge{&m_callGraph, m_curVxp, taskVtxp};
        if (isVirtualIfaceMethodCall(nodep) && isIfaceFTaskScope(getScope(nodep->taskp()))) {
            taskVtxp->needsNonInlineCFunc(true);
        }
        // Do we have to disable inlining the function?
        const V3TaskConnects tconnects = V3Task::taskConnects(nodep, nodep->taskp()->stmtsp());
        if (!taskVtxp->noInline()) {  // Else short-circuit below
            for (const auto& itr : tconnects) {
                const AstVar* const portp = itr.first;
                const AstArg* const argp = itr.second;
                if (const AstNodeExpr* const pinp = argp->exprp()) {
                    if ((portp->isRef() || portp->isConstRef()) && !VN_IS(pinp, VarRef)) {
                        UINFO(9, "No function inline due to ref " << pinp);
                        taskVtxp->noInline(true);
                    }
                }
            }
        }
    }
    void visit(AstNodeFTask* nodep) override {
        UINFO(9, "  TASK " << nodep);
        VL_RESTORER(m_curVxp);
        m_curVxp = getFTaskVertex(nodep);
        if (nodep->dpiImport()) m_curVxp->noInline(true);
        if (nodep->classMethod()) m_curVxp->noInline(true);  // Until V3Task supports it
        if (nodep->recursive()) m_curVxp->noInline(true);
        if (nodep->isConstructor()) {
            m_curVxp->noInline(true);
            m_ctorp = nodep;
            UASSERT_OBJ(m_classp, nodep, "Ctor not under class");
            m_funcToClassMap[nodep] = m_classp;
        }
        iterateChildren(nodep);
    }
    void visit(AstPragma* nodep) override {
        if (nodep->pragType() == VPragmaType::NO_INLINE_TASK) {
            // Just mark for the next steps, and we're done with it.
            m_curVxp->noInline(true);
            VL_DO_DANGLING(nodep->unlinkFrBack()->deleteTree(), nodep);
        } else {
            iterateChildren(nodep);
        }
    }
    void visit(AstVar* nodep) override {
        iterateChildren(nodep);
        nodep->user4p(m_curVxp);  // Remember what task it's under
    }
    void visit(AstVarRef* nodep) override {
        iterateChildren(nodep);
        AstVar* const varp = nodep->varp();
        if (varp->user4u().toGraphVertex() != m_curVxp) {
            if (m_curVxp->pure() && !varp->isXTemp() && !varp->isParam()) m_curVxp->impure(nodep);
        }
    }
    void visit(AstClass* nodep) override {
        // Move initial statements into the constructor
        VL_RESTORER(m_initialps);
        VL_RESTORER(m_ctorp);
        VL_RESTORER(m_classp);
        m_initialps.clear();
        m_ctorp = nullptr;
        m_classp = nodep;
        {  // Find m_initialps, m_ctor
            iterateChildren(nodep);
        }
        UASSERT_OBJ(m_ctorp, nodep, "class constructor missing");  // LinkDot always makes it
        AstNode* insertp = nullptr;
        for (AstInitialAutomatic* initialp : m_initialps) {
            if (AstNode* const movep = initialp->stmtsp()) {
                movep->unlinkFrBackWithNext();
                // Next InitialAutomatic must go in order after what we inserted
                insertp = AstNode::addNextNull(insertp, movep);
            }
            VL_DO_DANGLING(pushDeletep(initialp->unlinkFrBack()), initialp);
        }
        m_initialps.clear();
        if (insertp) {
            if (!m_ctorp->stmtsp()) {
                m_ctorp->addStmtsp(insertp);
            } else {
                m_ctorp->stmtsp()->addHereThisAsNext(insertp);
            }
        }
    }
    void visit(AstInitialAutomatic* nodep) override {
        m_initialps.push_back(nodep);
        iterateChildren(nodep);
    }
    //--------------------
    void visit(AstNode* nodep) override { iterateChildren(nodep); }

public:
    // CONSTRUCTORS
    explicit TaskStateVisitor(AstNetlist* nodep) {
        m_curVxp = new TaskCodeVertex{&m_callGraph};
        //
        iterate(nodep);
        //
        m_callGraph.removeRedundantEdgesSum(&TaskEdge::followAlwaysTrue);
        propagateImpureReason();
        decideInlining();
        if (dumpGraphLevel()) m_callGraph.dumpDotFilePrefixed("task_call");
    }
    ~TaskStateVisitor() override = default;
    VL_UNCOPYABLE(TaskStateVisitor);
};

//######################################################################
// DPI related utility functions

struct TaskDpiUtils final {
    // Returns a vector of ('elements', 'stride') pairs for each unpacked dimension
    static std::vector<std::pair<int, int>> unpackDimsAndStrides(AstVar* varp) {
        AstNodeDType* const dtypep = varp->dtypep()->skipRefp();
        std::vector<std::pair<int, int>> dimStrides;
        AstUnpackArrayDType* const unpackp = VN_CAST(dtypep->skipRefp(), UnpackArrayDType);
        if (!unpackp) return dimStrides;

        const std::vector<AstUnpackArrayDType*> dims = unpackp->unpackDimensions();
        const size_t nDims = dims.size();
        dimStrides.resize(nDims);
        // Stride of fastest varying dimension is 1.
        dimStrides[nDims - 1].first = dims.back()->elementsConst();
        dimStrides[nDims - 1].second = 1;
        // Rest are densly packed
        for (ssize_t i = nDims - 2; i >= 0; --i) {
            dimStrides[i].first = dims[i]->elementsConst();
            dimStrides[i].second = dimStrides[i + 1].first * dimStrides[i + 1].second;
        }
        return dimStrides;
    }
    // Returns the prefix of a function-call like statement used to convert
    // from IEEE DPI data types to internal types, and a bool that is true
    // if type uses svBitVecVal/svLogicVecVal and ther result is returned via
    // an output parameter, o false if uses C primitive type and result is the
    // return value.
    static std::pair<std::string, bool> dpiToInternalCvtStmt(AstVar* varp) {
        AstBasicDType* const basicp = varp->basicp();

        // DPI types using Primitive C types
        if (basicp->keyword() == VBasicDTypeKwd::CHANDLE) return {"VL_CVT_VP_Q(", false};
        if (basicp->keyword() == VBasicDTypeKwd::STRING) return {"VL_CVT_N_CSTR(", false};
        if (basicp->isDpiPrimitive()) return {"(", false};

        // DPI types using svBitVecVal/svLogicVecVal
        UASSERT_OBJ(basicp->isDpiBitVec() || basicp->isDpiLogicVec(), varp,
                    "Should use svBitVecVal/svLogicVecVal");

        const std::string vecType = basicp->isDpiBitVec() ? "SVBV" : "SVLV";
        const AstNodeDType* const dtypep = varp->dtypep()->skipRefp();
        const char sizeChar = dtypep->width() <= 8    ? 'C'
                              : dtypep->width() <= 16 ? 'S'
                                                      : *dtypep->charIQWN();
        const std::string& size = std::to_string(dtypep->width());
        // cppcheck-suppress strPlusChar
        return {"VL_SET_"s + sizeChar + "_" + vecType + "(" + size + ", ", true};
    }
};

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

class TaskVisitor final : public VNVisitor {
    // NODE STATE
    // Each module:
    //    AstNodeFTask::user1   // True if its been expanded
    // Each funccall
    //  to 'relink' function:
    //    AstVar::user2p        // AstVarScope* to replace varref with
    const VNUser1InUse m_inuser1;
    const VNUser2InUse m_inuser2;

    // TYPES
    using DpiCFuncs = std::map<const string, std::tuple<AstNodeFTask*, std::string, AstCFunc*>>;

    // STATE
    TaskStateVisitor* const m_statep;  // Common state between visitors
    V3UniqueNames m_initArrayTmpNames;  // For generating unique temporary variable names for
                                        // arguments being AstInitArray
    AstNodeModule* m_modp = nullptr;  // Current module
    AstTopScope* const m_topScopep = v3Global.rootp()->topScopep();  // The AstTopScope
    AstScope* m_scopep = nullptr;  // Current scope
    AstNode* m_insStmtp = nullptr;  // Where to insert statement
    bool m_inSensesp = false;  // Are we under a senitem?
    bool m_inNew = false;  // Are we under a constructor?
    int m_modNCalls = 0;  // Incrementing func # for making symbols
    int m_unconVarNum = 0;  // Unique bad connection variable

    // STATE - across all visitors
    V3UniqueNames m_forceTmpNames;  // For generating unique force-RHS helper names
    DpiCFuncs m_dpiNames;  // Map of all created DPI functions
    VDouble0 m_statInlines;  // Statistic tracking
    VDouble0 m_statHierDpisWithCosts;  // Statistic tracking

    // METHODS

    AstVarScope* createFuncVar(AstCFunc* funcp, const string& name, AstVar* examplep) {
        AstVar* const newvarp = new AstVar{funcp->fileline(), VVarType::BLOCKTEMP, name, examplep};
        newvarp->funcLocal(true);
        funcp->addVarsp(newvarp);
        AstVarScope* const newvscp = new AstVarScope{funcp->fileline(), m_scopep, newvarp};
        m_scopep->addVarsp(newvscp);
        return newvscp;
    }
    AstVarScope* createInputVar(AstCFunc* funcp, const string& name, VBasicDTypeKwd kwd) {
        AstVar* const newvarp
            = new AstVar{funcp->fileline(), VVarType::BLOCKTEMP, name, funcp->findBasicDType(kwd)};
        newvarp->funcLocal(true);
        newvarp->direction(VDirection::INPUT);
        funcp->addArgsp(newvarp);
        AstVarScope* const newvscp = new AstVarScope{funcp->fileline(), m_scopep, newvarp};
        m_scopep->addVarsp(newvscp);
        return newvscp;
    }
    AstVarScope* createVarScope(AstVar* invarp, const string& name) {
        if (invarp->isParam() && VN_IS(invarp->valuep(), InitArray)) {
            // Move array params in functions into constant pool
            return v3Global.rootp()->constPoolp()->findTable(VN_AS(invarp->valuep(), InitArray));
        } else {
            // We could create under either the ref's scope or the ftask's scope.
            // It shouldn't matter, as they are only local variables.
            // We choose to do it under whichever called this function, which results
            // in more cache locality.
            AstVar* const newvarp
                = new AstVar{invarp->fileline(), VVarType::BLOCKTEMP, name, invarp};
            newvarp->funcLocal(false);
            newvarp->propagateAttrFrom(invarp);
            newvarp->isInternal(true);
            m_modp->addStmtsp(newvarp);
            AstVarScope* const newvscp = new AstVarScope{newvarp->fileline(), m_scopep, newvarp};
            m_scopep->addVarsp(newvscp);
            return newvscp;
        }
    }

    // Replace varrefs with new var pointer
    void relink(AstNode* nodep) {
        nodep->foreachAndNext([](AstVarRef* refp) {
            if (refp->varp()->user2p()) {  // It's being converted to an alias.
                AstVarScope* const newvscp = VN_AS(refp->varp()->user2p(), VarScope);
                refp->varScopep(newvscp);
                refp->varp(refp->varScopep()->varp());
            }
        });
    }

    AstAssign* connectPortMakeInAssign(AstNodeExpr* pinp, AstVarScope* newvscp, bool pureCheck) {
        // Create input assignment to go in FRONT of function call
        AstNodeExpr* inPinp = pinp;
        if (AstResizeLValue* sinPinp = VN_CAST(inPinp, ResizeLValue)) inPinp = sinPinp->lhsp();
        AstNodeExpr* const inPinClonep
            = pureCheck ? inPinp->cloneTreePure(true) : inPinp->cloneTree(true);
        AstAssign* const assp = new AstAssign{
            pinp->fileline(), new AstVarRef{newvscp->fileline(), newvscp, VAccess::WRITE},
            inPinClonep};
        assp->fileline()->modifyWarnOff(V3ErrorCode::BLKSEQ, true);  // Ok if in <= block
        return assp;
    }
    AstAssign* connectPortMakeOutAssign(AstVar* portp, AstNodeExpr* pinp, AstVarScope* newvscp,
                                        bool pureCheck) {
        // If needed, remap size of function to caller's output size
        AstNodeExpr* outPinp = pinp;
        AstNodeExpr* postRhsp = new AstVarRef{newvscp->fileline(), newvscp, VAccess::READ};
        if (AstResizeLValue* soutPinp = VN_CAST(outPinp, ResizeLValue)) {
            outPinp = soutPinp->lhsp();
            if (AstNodeUniop* aoutPinp = VN_CAST(outPinp, Extend)) {
                outPinp = aoutPinp->lhsp();
            } else if (AstNodeUniop* aoutPinp = VN_CAST(outPinp, ExtendS)) {
                outPinp = aoutPinp->lhsp();
            } else if (AstSel* aoutPinp = VN_CAST(outPinp, Sel)) {
                outPinp = aoutPinp->fromp();
            } else {
                outPinp->v3fatalSrc("Inout pin resizing should have had extend or select");
            }
            if (outPinp->width() < portp->width()) {
                postRhsp = new AstSel{pinp->fileline(), postRhsp, 0, pinp->width()};
            } else {  // pin width > port width
                if (pinp->isSigned() && postRhsp->isSigned()) {
                    postRhsp = new AstExtendS{pinp->fileline(), postRhsp};
                } else {
                    postRhsp = new AstExtend{pinp->fileline(), postRhsp};
                }
            }
            postRhsp->dtypeFrom(outPinp);
        }
        // Put output assignment AFTER function call
        AstNodeExpr* const outPinClonep
            = pureCheck ? outPinp->cloneTreePure(true) : outPinp->cloneTree(true);
        AstAssign* const assp = new AstAssign{pinp->fileline(), outPinClonep, postRhsp};
        assp->fileline()->modifyWarnOff(V3ErrorCode::BLKSEQ, true);  // Ok if in <= block
        return assp;
    }

    void changeAtWriteRecurse(AstNodeExpr* const exprp) {
        // Change nested at methods to writable variant
        if (AstCMethodHard* const cMethodp = VN_CAST(exprp, CMethodHard)) {
            if (cMethodp->method() == VCMethod::ARRAY_AT) {
                cMethodp->method(VCMethod::ARRAY_AT_WRITE);
            }
            changeAtWriteRecurse(cMethodp->fromp());
        }
    }

    void connectPort(AstVar* portp, AstArg* argp, const string& namePrefix, AstNode* beginp,
                     bool inlineTask) {
        AstNodeExpr* pinp = argp->exprp();
        if (inlineTask) {
            portp->unlinkFrBack();
            pushDeletep(portp);  // Remove it from the clone (not original)
        }
        if (!pinp) {
            // Too few arguments in function call
        } else {
            UINFO(9, "     Port " << portp);
            UINFO(9, "      pin " << pinp);
            if (portp->isWritable() && VN_IS(pinp, Const)) {
                pinp->v3error("Function/task " + portp->direction().prettyName()  // e.g. "output"
                              + " connected to constant instead of variable: "
                              + portp->prettyNameQ());
                // Make temp pin to tie it off
                AstVar* const varp = new AstVar{pinp->fileline(), VVarType::STMTTEMP,
                                                "__VfuncUnconn_" + portp->name() + "__"
                                                    + std::to_string(m_unconVarNum++),
                                                portp->dtypep()};
                m_modp->addStmtsp(varp);
                AstVarScope* const newvscp = new AstVarScope{pinp->fileline(), m_scopep, varp};
                m_scopep->addVarsp(newvscp);
                AstVarRef* const repp = new AstVarRef{pinp->fileline(), newvscp, VAccess::WRITE};
                pinp->replaceWith(repp);
                pushDeletep(pinp);
                pinp = repp;
            }
            if (inlineTask) {
                pushDeletep(pinp->unlinkFrBack());  // Cloned in assignment below
                VL_DO_DANGLING(argp->unlinkFrBack()->deleteTree(), argp);  // Args no longer needed
            }
            if (portp->isRef() || portp->isConstRef()) {
                bool refArgOk = false;
                if (VN_IS(pinp, VarRef) || VN_IS(pinp, MemberSel) || VN_IS(pinp, StructSel)
                    || VN_IS(pinp, ArraySel)) {
                    refArgOk = true;
                } else if (AstCMethodHard* const cMethodp = VN_CAST(pinp, CMethodHard)) {
                    if (VN_IS(cMethodp->fromp()->dtypep()->skipRefp(), QueueDType)) {
                        refArgOk = cMethodp->method() == VCMethod::DYN_AT_WRITE_APPEND
                                   || cMethodp->method() == VCMethod::DYN_AT_WRITE_APPEND_BACK;
                    } else {
                        changeAtWriteRecurse(cMethodp);
                        refArgOk = cMethodp->method() == VCMethod::ARRAY_AT_WRITE;
                    }
                }
                if (refArgOk) {
                    if (AstVarRef* const varrefp = VN_CAST(pinp, VarRef)) {
                        varrefp->access(VAccess::READWRITE);
                    }
                } else {
                    pinp->v3error("Function/task ref argument is not of allowed type");
                }
                if (inlineTask) {
                    if (AstVarRef* const varrefp = VN_CAST(pinp, VarRef)) {
                        // Connect to this exact variable
                        AstVarScope* const localVscp = varrefp->varScopep();
                        UASSERT_OBJ(localVscp, varrefp, "Null var scope");
                        portp->user2p(localVscp);
                    } else {
                        pinp->v3fatalSrc("ref argument should have caused non-inline of function");
                    }
                }
            } else if (portp->isInout()) {
                // UINFOTREE(9, pinp, "", "pinrsize-");

                AstVarScope* const newvscp
                    = createVarScope(portp, namePrefix + "__" + portp->shortName());
                portp->user2p(newvscp);
                if (!inlineTask) {
                    pinp->replaceWith(
                        new AstVarRef{newvscp->fileline(), newvscp, VAccess::READWRITE});
                    pushDeletep(pinp);  // Cloned by connectPortMakeInAssign
                }
                // Put input assignment in FRONT of all other statements
                AstAssign* const preassp = connectPortMakeInAssign(pinp, newvscp, true);
                if (AstNode* const afterp = beginp->nextp()) {
                    afterp->unlinkFrBackWithNext();
                    AstNode::addNext<AstNode, AstNode>(preassp, afterp);
                }
                beginp->addNext(preassp);

                AstAssign* const postassp = connectPortMakeOutAssign(portp, pinp, newvscp, true);
                beginp->addNext(postassp);
                // if (debug() >= 9) beginp->dumpTreeAndNext(cout, "-pinrsize-out- ");
            } else if (portp->isWritable()) {
                // Even if it's referencing a varref, we still make a temporary
                // Else task(x,x,x) might produce incorrect results
                AstVarScope* const newvscp
                    = createVarScope(portp, namePrefix + "__" + portp->shortName());
                portp->user2p(newvscp);
                if (!inlineTask) {
                    pinp->replaceWith(new AstVarRef{newvscp->fileline(), newvscp, VAccess::WRITE});
                    pushDeletep(pinp);  // Cloned by connectPortMakeOutAssign
                }
                AstAssign* const postassp = connectPortMakeOutAssign(portp, pinp, newvscp, false);
                // Put assignment BEHIND of all other statements
                beginp->addNext(postassp);
            } else if (inlineTask && portp->isNonOutput()) {
                // Make input variable
                AstVarScope* const newvscp
                    = createVarScope(portp, namePrefix + "__" + portp->shortName());
                portp->user2p(newvscp);
                AstAssign* const preassp = connectPortMakeInAssign(pinp, newvscp, false);
                // Put assignment in FRONT of all other statements
                if (AstNode* const afterp = beginp->nextp()) {
                    afterp->unlinkFrBackWithNext();
                    AstNode::addNext<AstNode, AstNode>(preassp, afterp);
                }
                beginp->addNext(preassp);
            }
        }
    }

    bool hasRefArgument(AstNodeFTask* nodep) {
        for (AstNode* stmtp = nodep->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
            if (AstVar* const varp = VN_CAST(stmtp, Var)) {
                if (varp->isRef() || varp->isConstRef()) return true;
            }
        }
        return false;
    }

    AstNode* createInlinedFTask(AstNodeFTaskRef* refp, const string& namePrefix,
                                AstVarScope* outvscp) {
        // outvscp is the variable for functions only, if nullptr, it's a task
        UASSERT_OBJ(refp->taskp(), refp, "Unlinked?");
        AstNode* const newbodysp
            = refp->taskp()->stmtsp() ? refp->taskp()->stmtsp()->cloneTree(true) : nullptr;
        AstNode* const beginp
            = new AstComment{refp->fileline(), "Function: "s + refp->name(), true};
        if (newbodysp) beginp->addNext(newbodysp);
        if (debug() >= 9) beginp->dumpTreeAndNext(cout, "-  newbegi: ");
        //
        // Create input variables
        AstNode::user2ClearTree();
        {
            const V3TaskConnects tconnects = V3Task::taskConnects(refp, beginp);
            for (const auto& itr : tconnects) {
                AstVar* const portp = itr.first;
                AstArg* const argp = itr.second;
                connectPort(portp, argp, namePrefix, beginp, true);
            }
        }
        UASSERT_OBJ(!refp->argsp(), refp, "Arg wasn't removed by above loop");
        {
            AstNode* nextstmtp;
            for (AstNode* stmtp = beginp; stmtp; stmtp = nextstmtp) {
                nextstmtp = stmtp->nextp();
                if (AstVar* const portp = VN_CAST(stmtp, Var)) {
                    // Any I/O variables that fell out of above loop were already linked
                    if (!portp->user2p()) {
                        // Move it to a new localized variable
                        AstVarScope* const localVscp
                            = createVarScope(portp, namePrefix + "__" + portp->shortName());
                        portp->user2p(localVscp);
                        if (portp->needsCReset() && portp->lifetime().isAutomatic()
                            && !portp->valuep()) {
                            // Reset automatic var to its default, on each invocation of function
                            AstNode* const crstp = new AstAssign{
                                portp->fileline(),
                                new AstVarRef{portp->fileline(), portp, VAccess::WRITE},
                                new AstCReset{portp->fileline(), portp, false}};
                            portp->replaceWith(crstp);
                        } else {
                            portp->unlinkFrBack();
                        }
                        pushDeletep(portp);  // Remove it from the clone (not original)
                    }
                }
            }
        }
        // Create function output variables
        if (outvscp) {
            // UINFO(0, "setflag on " << funcp->fvarp() << " to " << outvscp);
            refp->taskp()->fvarp()->user2p(outvscp);
        }
        // Replace variable refs
        relink(beginp);
        //
        if (debug() >= 9) beginp->dumpTreeAndNext(cout, "-  iotask: ");
        return beginp;
    }

    AstNode* createNonInlinedFTask(AstNodeFTaskRef* refp, const string& namePrefix,
                                   AstVarScope* outvscp, AstCNew*& cnewpr) {
        // outvscp is the variable for functions only, if nullptr, it's a task
        UASSERT_OBJ(refp->taskp(), refp, "Unlinked?");
        AstCFunc* const cfuncp = m_statep->ftaskCFuncp(refp->taskp());
        UASSERT_OBJ(cfuncp, refp, "No non-inline task associated with this task call?");
        //
        AstNode* const beginp
            = new AstComment{refp->fileline(), "Function: "s + refp->name(), true};
        AstNodeCCall* ccallp;
        if (VN_IS(refp, New)) {
            AstCNew* const cnewp = new AstCNew{refp->fileline(), cfuncp};
            cnewp->dtypep(refp->dtypep());
            ccallp = cnewp;
            // Parent AstNew will replace with this CNew
            cnewpr = cnewp;
        } else if (const AstMethodCall* const mrefp = VN_CAST(refp, MethodCall)) {
            ccallp = new AstCMethodCall{refp->fileline(), mrefp->fromp()->unlinkFrBack(), cfuncp};
            ccallp->dtypeSetVoid();
            beginp->addNext(ccallp->makeStmt());
        } else {
            ccallp = new AstCCall{refp->fileline(), cfuncp};
            ccallp->dtypeSetVoid();
            beginp->addNext(ccallp->makeStmt());
        }

        if (const AstFuncRef* const funcRefp = VN_CAST(refp, FuncRef)) {
            ccallp->superReference(funcRefp->superReference());
        } else if (const AstTaskRef* const taskRefp = VN_CAST(refp, TaskRef)) {
            ccallp->superReference(taskRefp->superReference());
        }

        // Convert complicated outputs to temp signals
        {
            const V3TaskConnects tconnects = V3Task::taskConnects(refp, refp->taskp()->stmtsp());
            for (const auto& itr : tconnects) {
                AstVar* const portp = itr.first;
                AstArg* const argp = itr.second;
                connectPort(portp, argp, namePrefix, beginp, false);
            }
        }
        // First argument is symbol table, then output if a function
        const bool needSyms = !refp->taskp()->dpiImport() || v3Global.opt.profExec();
        if (needSyms) ccallp->argTypes("vlSymsp");

        if (refp->taskp()->dpiContext()) {
            AstScopeName* const snp = refp->scopeNamep();
            UASSERT_OBJ(snp, refp, "Missing scoping context");
            snp->unlinkFrBack();
            FileLine* const flp = refp->fileline();
            // __Vscopep
            ccallp->addArgsp(snp);
            // __Vfilenamep
            ccallp->addArgsp(
                new AstCExpr{flp, AstCExpr::Pure{}, "\"" + flp->filenameEsc() + "\"", 64});
            // __Vlineno
            ccallp->addArgsp(new AstConst(flp, flp->lineno()));
        }

        // Move artument expression to the CCall, without AstArg
        for (AstArg *argp = refp->argsp(), *nextp; argp; argp = nextp) {
            nextp = VN_AS(argp->nextp(), Arg);
            ccallp->addArgsp(argp->exprp()->unlinkFrBack());
        }

        if (outvscp) ccallp->addArgsp(new AstVarRef{refp->fileline(), outvscp, VAccess::WRITE});

        if (debug() >= 9) beginp->dumpTreeAndNext(cout, "-  nitask: ");
        return beginp;
    }

    string dpiSignature(AstNodeFTask* nodep, AstVar* rtnvarp) const {
        // Return fancy signature for DPI function. Variable names are not included so differences
        // in only argument names will not matter (as required by the standard).
        string dpiproto;
        if (nodep->dpiPure()) dpiproto += "pure ";
        if (nodep->dpiContext()) dpiproto += "context ";
        dpiproto += rtnvarp ? rtnvarp->dpiArgType(true, true) : "void";
        dpiproto += " " + nodep->cname() + " (";
        string args;
        for (AstNode* stmtp = nodep->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
            if (const AstVar* const portp = VN_CAST(stmtp, Var)) {
                if (portp->isIO() && !portp->isFuncReturn() && portp != rtnvarp) {
                    if (args != "") {
                        args += ", ";
                        dpiproto += ", ";
                    }
                    // Include both the Verilator and C type names, as if either
                    // differ we may get C compilation problems later
                    const std::string dpiType = portp->dpiArgType(false, false);
                    dpiproto += dpiType;
                    const std::string vType = portp->dtypep()->prettyDTypeName(false);
                    if (!portp->isDpiOpenArray() && dpiType != vType) {
                        dpiproto += " /* " + vType + " */ ";
                    }
                    args += portp->name();  // Leftover so ,'s look nice
                }
            }
        }
        dpiproto += ")";
        return dpiproto;
    }

    static void checkLegalCIdentifier(AstNode* nodep, const string& name) {
        if (name.end() != std::find_if(name.begin(), name.end(), [](char c) {
                return !std::isalnum(c) && c != '_';
            })) {
            nodep->v3error("DPI function has illegal characters in C identifier name '" << name
                                                                                        << '\'');
        }
    }

    static AstNode* createDpiTemp(AstVar* portp, const string& suffix) {
        const string stmt = portp->dpiTmpVarType(portp->name() + suffix) + ";";
        return new AstCStmt{portp->fileline(), stmt};
    }

    void unlinkAndClone(AstNodeFTask* funcp, AstNode* nodep, bool withNext) {
        UASSERT_OBJ(nodep, funcp, "null in function object clone");
        if (funcp->recursive()) {
            VNRelinker relinkHandle;
            if (withNext) {
                nodep->unlinkFrBackWithNext(&relinkHandle);
            } else {
                nodep->unlinkFrBack(&relinkHandle);
            }
            // Recursive functions require the original argument list to
            // still be live for linking purposes.
            // The old function gets clone, so that node pointers are mostly
            // retained through the V3Task transformations
            AstNode* const newp = nodep->cloneTree(withNext);
            relinkHandle.relink(newp);
        } else {
            if (withNext) {
                nodep->unlinkFrBackWithNext();
            } else {
                nodep->unlinkFrBack();
            }
        }
    }

    static AstNode* createAssignInternalToDpi(AstVar* portp, bool isPtr, const string& frSuffix,
                                              const string& toSuffix) {
        const string stmt = V3Task::assignInternalToDpi(portp, isPtr, frSuffix, toSuffix);
        return new AstCStmt{portp->fileline(), stmt};
    }

    AstNodeStmt* createAssignDpiToInternal(AstVarScope* vscp, const std::string& rhsName) {
        // Create assignment from DPI temporary into internal format
        // DPI temporary is scalar or 1D array (if unpacked array)
        // Internal representation is scalar, 1D, or multi-dimensional array (similar to SV)
        AstVar* const varp = vscp->varp();
        FileLine* const flp = vscp->fileline();
        std::string cvt;
        bool useSvVec;
        std::tie(cvt, useSvVec) = TaskDpiUtils::dpiToInternalCvtStmt(varp);
        const std::vector<std::pair<int, int>> strides = TaskDpiUtils::unpackDimsAndStrides(varp);
        // Total number of elements in unpacked array
        const int total = strides.empty() ? 1 : strides[0].first * strides[0].second;
        // Number of words per element/primitive type
        const int widthWords = varp->basicp()->widthWords();
        // Number of bits in the C expression result, for masking.
        const int cwidth = widthWords * VL_EDATASIZE;
        // The resulting list of statements
        AstNodeStmt* stmtsp = nullptr;
        for (int i = 0; i < total; ++i) {
            AstNodeExpr* lhsp = new AstVarRef{flp, vscp, VAccess::WRITE};

            // Extract a scalar from multi-dimensional array (internal format)
            for (const auto& stride : strides) {
                lhsp = new AstArraySel(flp, lhsp, (i / stride.second) % stride.first);
            }

            // Extract a scalar from DPI temporary var that is scalar or 1D array
            if (useSvVec) {
                AstCStmt* const cstmtp = new AstCStmt{flp};
                cstmtp->add(cvt);
                cstmtp->add(lhsp);
                cstmtp->add(", " + rhsName + " + " + std::to_string(i * widthWords) + ");");
                stmtsp = AstNode::addNext(stmtsp, cstmtp);
            } else {
                const std::string elem = strides.empty() ? "" : "[" + std::to_string(i) + "]";
                AstNodeExpr* rhsp = new AstCExpr{flp, cvt + rhsName + elem + ")", cwidth};
                rhsp = new AstSel{flp, rhsp, 0, varp->width()};
                stmtsp = AstNode::addNext(stmtsp, new AstAssign{flp, lhsp, rhsp});
            }
        }
        return stmtsp;
    }

    // Create dispatch wrapper
    AstCFunc* makeDpiExportDispatcher(AstNodeFTask* const nodep, AstVar* const rtnvarp) {
        // Verilog name has __ conversion and other tricks, to match DPI C code, back that out
        FileLine* const flp = nodep->fileline();
        const std::string cname = AstNode::prettyName(nodep->cname());
        checkLegalCIdentifier(nodep, cname);
        const char* const tmpSuffixp = V3Task::dpiTemporaryVarSuffix();
        const std::string rtnType = rtnvarp ? rtnvarp->dpiArgType(true, true) : "";
        // The function we are building
        AstCFunc* const funcp = new AstCFunc{flp, cname, m_scopep, rtnType};
        funcp->dpiExportDispatcher(true);
        funcp->dpiContext(nodep->dpiContext());
        funcp->dontCombine(true);
        funcp->entryPoint(true);
        funcp->isStatic(true);
        funcp->protect(false);
        funcp->cname(cname);
        // Add DPI Export to top, since it's a global function
        m_topScopep->scopep()->addBlocksp(funcp);

        // Note this function may dispatch on a different class.
        // Thus we need to be careful not to assume a particular function layout.
        //
        // Func numbers must be the same for each function, even when there are
        // completely different models with the same function name.
        // Thus we can't just use a constant computed at Verilation time.
        // We could use 64-bits of a MD5/SHA hash rather than a string here,
        // but the compare is only done on first call then memoized, so
        // it's not worth optimizing.

        // Peramble - fetch the exproted function from the scope table
        AstCStmt* const prep = new AstCStmt{flp};
        funcp->addStmtsp(prep);
        // Static doesn't need save-restore as if below will re-fill proper value
        prep->add("static int __Vfuncnum = -1;\n");
        // First time init (faster than what the compiler does if we did a singleton
        prep->add("if (VL_UNLIKELY(__Vfuncnum == -1)) {\n");
        prep->add("__Vfuncnum = Verilated::exportFuncNum(\"" + nodep->cname() + "\");\n");
        prep->add("}\n");
        // If the find fails, it will throw an error
        prep->add("const VerilatedScope* const __Vscopep = Verilated::dpiScope();\n");
        // If 'dpiScope()' fails and '__Vscopep' is null; the exportFind function throws an error
        // If __Vcb is null the exportFind function throws and error
        const std::string cbtype
            = VIdProtect::protect(v3Global.opt.prefix() + "__Vcb_" + nodep->cname() + "_t");
        prep->add(cbtype + " __Vcb = reinterpret_cast<" + cbtype
                  + ">(VerilatedScope::exportFind(__Vscopep, __Vfuncnum));");

        // Convert input/inout DPI arguments to Internal types, and construct the call
        AstCStmt* const callp = new AstCStmt{flp};
        const auto addFuncArg = [&](AstVar* portp) -> AstVarScope* {
            // No createDpiTemp; we make a real internal variable instead
            AstVarScope* const vscp = createFuncVar(funcp, portp->name() + tmpSuffixp, portp);
            // No information exposure; is already visible in import/export func template
            vscp->varp()->protect(false);
            portp->protect(false);
            // Add argument to call
            const VAccess access = portp->isWritable() ? VAccess::WRITE : VAccess::READ;
            callp->add(", ");
            callp->add(new AstVarRef{portp->fileline(), vscp, access});
            return vscp;
        };
        // Call callback
        callp->add("(*__Vcb)(");
        // First argument is the Syms
        callp->add("(" + EmitCUtil::symClassName() + "*)(__Vscopep->symsp())");
        // Add function arguments
        for (AstNode* stmtp = nodep->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
            AstVar* const portp = VN_CAST(stmtp, Var);
            if (!portp) continue;
            if (!portp->isIO()) continue;
            if (portp->isFuncReturn()) continue;
            if (portp == rtnvarp) continue;  // Handled below
            // Add argument to call
            AstVarScope* const outvscp = addFuncArg(portp);
            if (!portp->isNonOutput()) continue;
            // Convert input/inout arguments to dpi type
            const std::string deref
                = portp->isInout()  //
                          && portp->basicp()->isDpiPrimitive()  //
                          && portp->dtypep()->skipRefp()->dimensions(false).second == 0
                      ? "*"
                      : "";
            funcp->addStmtsp(createAssignDpiToInternal(outvscp, deref + portp->name()));
        }
        // Return value argument goes last
        if (rtnvarp) addFuncArg(rtnvarp);
        // Close statement
        callp->add(");");
        // Call the user function
        funcp->addStmtsp(callp);
        // Convert output/inout arguments back to internal type
        for (AstNode* stmtp = nodep->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
            if (AstVar* const portp = VN_CAST(stmtp, Var)) {
                if (portp->isIO() && portp->isWritable() && !portp->isFuncReturn()) {
                    funcp->addStmtsp(createAssignInternalToDpi(portp, true, tmpSuffixp, ""));
                }
            }
        }
        // Convert return value
        if (rtnvarp) {
            funcp->addStmtsp(createDpiTemp(rtnvarp, ""));
            funcp->addStmtsp(createAssignInternalToDpi(rtnvarp, false, tmpSuffixp, ""));
            const std::string index = rtnvarp->basicp()->isDpiPrimitive() ? "" : "[0]";
            funcp->addStmtsp(new AstCStmt{flp, "return " + rtnvarp->name() + index + ";"});
        }
        if (!makePortList(nodep, funcp)) return nullptr;
        return funcp;
    }

    AstCFunc* makeDpiImportPrototype(AstNodeFTask* nodep, AstVar* rtnvarp) {
        // Verilog name has __ conversion and other tricks, to match DPI C code, back that out
        const string name = AstNode::prettyName(nodep->cname());
        checkLegalCIdentifier(nodep, name);
        // Tasks (but not void functions) return a boolean 'int' indicating disabled
        const string rtnType = rtnvarp            ? rtnvarp->dpiArgType(true, true)
                               : nodep->dpiTask() ? "int"
                                                  : "";
        AstCFunc* const funcp = new AstCFunc{nodep->fileline(), name, m_scopep, rtnType};
        funcp->dpiContext(nodep->dpiContext());
        funcp->dpiImportPrototype(true);
        funcp->dontCombine(true);
        funcp->entryPoint(false);
        funcp->isMethod(false);
        funcp->protect(false);
        funcp->dpiPure(nodep->dpiPure());

        const int cost = static_cast<int>(V3Control::getProfileData(funcp->name()));
        m_statHierDpisWithCosts += (cost != 0);
        funcp->cost(cost);

        // Add DPI Import to top, since it's a global function
        m_topScopep->scopep()->addBlocksp(funcp);
        if (!makePortList(nodep, funcp)) return nullptr;
        return funcp;
    }

    AstCFunc* getDpiFunc(AstNodeFTask* nodep, AstVar* rtnvarp) {
        UASSERT_OBJ(nodep->dpiImport() || nodep->dpiExport(), nodep, "Not a DPI function");
        // Compute unique signature of this DPI function
        const string signature = dpiSignature(nodep, rtnvarp);
        // Only create one DPI Import prototype or DPI Export entry point for each unique cname as
        // it is illegal for the user to attach multiple tasks with different signatures to one DPI
        // cname.
        const auto pair = m_dpiNames.emplace(std::piecewise_construct,  //
                                             std::forward_as_tuple(nodep->cname()),
                                             std::forward_as_tuple(nodep, signature, nullptr));
        if (pair.second) {
            // First time encountering this cname. Create Import prototype / Export entry point
            AstCFunc* const funcp = nodep->dpiExport() ? makeDpiExportDispatcher(nodep, rtnvarp)
                                                       : makeDpiImportPrototype(nodep, rtnvarp);
            std::get<2>(pair.first->second) = funcp;
            return funcp;
        } else {
            // Seen this cname import before. Check if it's the same prototype.
            const AstNodeFTask* firstNodep;
            string firstSignature;
            AstCFunc* firstFuncp;
            std::tie(firstNodep, firstSignature, firstFuncp) = pair.first->second;
            if (signature != firstSignature) {
                // Different signature, so error.
                nodep->v3error("Duplicate declaration of DPI function with different signature: '"
                               << nodep->cname() << "'\n"
                               << nodep->warnContextPrimary() << '\n'
                               << nodep->warnMore()  //
                               << "... New signature:      " << signature << '\n'  //
                               << firstNodep->warnOther()
                               << "... Original signature: " << firstSignature << '\n'  //
                               << firstNodep->warnContextSecondary());
                return nullptr;
            } else {
                // Same signature, return the previously created CFunc
                return firstFuncp;
            }
        }
    }

    static bool makePortList(AstNodeFTask* nodep, AstCFunc* dpip) {
        bool allOk = true;
        // Copy nodep's list of function I/O to the new dpip c function
        for (AstNode* stmtp = nodep->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
            if (AstVar* const portp = VN_CAST(stmtp, Var)) {
                if (portp->isIO()) {
                    // Move it to new function
                    AstVar* const newPortp = portp->cloneTree(false);
                    newPortp->funcLocal(true);
                    dpip->addArgsp(newPortp);
                    if (!portp->basicp()) {
                        allOk = false;
                        portp->v3warn(
                            E_UNSUPPORTED,
                            "Unsupported: DPI argument of type "
                                << portp->dtypep()->prettyTypeName() << '\n'
                                << portp->warnMore()
                                << "... For best portability, use bit, byte, int, or longint");
                        // We don't warn on logic either, although the 4-stateness is lost.
                        // That's what other simulators do.
                    }
                }
            }
        }
        return allOk;
    }

    void bodyDpiImportFunc(AstNodeFTask* nodep, AstVarScope* rtnvscp, AstCFunc* cfuncp,
                           AstCFunc* dpiFuncp) {
        const char* const tmpSuffixp = V3Task::dpiTemporaryVarSuffix();

        if (v3Global.opt.profExec()) {
            cfuncp->addStmtsp(AstCStmt::profExecSectionPush(nodep->fileline(), "dpiimports"));
        }

        // Convert input/inout arguments to DPI types
        string args;
        for (AstNode* stmtp = cfuncp->argsp(); stmtp; stmtp = stmtp->nextp()) {
            if (AstVar* const portp = VN_CAST(stmtp, Var)) {
                AstVarScope* const portvscp
                    = VN_AS(portp->user2p(), VarScope);  // Remembered when we created it earlier
                if (portp->isIO() && !portp->isFuncReturn() && portvscp != rtnvscp
                    && portp->name() != "__Vscopep"  // Passed to dpiContext, not callee
                    && portp->name() != "__Vfilenamep" && portp->name() != "__Vlineno") {

                    if (args != "") args += ", ";

                    if (portp->isDpiOpenArray()) {
                        AstNodeDType* const dtypep = portp->dtypep()->skipRefp();
                        UASSERT_OBJ(!VN_IS(dtypep, DynArrayDType) && !VN_IS(dtypep, QueueDType),
                                    portp,
                                    "Passing dynamic array or queue as actual argument to DPI "
                                    "open array is not yet supported");
                        // Ideally we'd make a table of variable
                        // characteristics, and reuse it wherever we can
                        // At least put them into the module's CTOR as static?
                        const string propName = portp->name() + "__Vopenprops";
                        const string propCode = portp->vlPropDecl(propName);
                        cfuncp->addStmtsp(new AstCStmt{portp->fileline(), propCode});
                        //
                        // At runtime we need the svOpenArrayHandle to
                        // point to this task & thread's data, in addition
                        // to static info about the variable
                        const string name = portp->name() + "__Vopenarray";
                        const string varCode
                            = ("VerilatedDpiOpenVar "
                               // NOLINTNEXTLINE(performance-inefficient-string-concatenation)
                               + name + " (&" + propName + ", &" + portp->name() + ");\n");
                        cfuncp->addStmtsp(new AstCStmt{portp->fileline(), varCode});
                        args += "&" + name;
                    } else {
                        if (portp->isWritable() && portp->basicp()->isDpiPrimitive()) {
                            if (!VN_IS(portp->dtypep()->skipRefp(), UnpackArrayDType)) args += "&";
                        }

                        args += portp->name() + tmpSuffixp;

                        cfuncp->addStmtsp(createDpiTemp(portp, tmpSuffixp));
                        if (portp->isNonOutput()) {
                            cfuncp->addStmtsp(
                                createAssignInternalToDpi(portp, false, "", tmpSuffixp));
                        }
                    }
                }
            }
        }

        // Store context, if needed
        if (nodep->dpiContext()) {
            const string stmt = "Verilated::dpiContext(__Vscopep, __Vfilenamep, __Vlineno);";
            cfuncp->addStmtsp(new AstCStmt{nodep->fileline(), stmt});
        }

        {  // Call the imported function
            AstCCall* const callp = new AstCCall{nodep->fileline(), dpiFuncp};
            callp->dtypeSetVoid();
            callp->argTypes(args);
            if (rtnvscp) {
                // If it has a return value, capture it
                cfuncp->addStmtsp(createDpiTemp(rtnvscp->varp(), tmpSuffixp));
                const std::string sel = rtnvscp->varp()->basicp()->isDpiPrimitive() ? "" : "[0]";
                AstCStmt* const cstmtp = new AstCStmt{nodep->fileline()};
                cstmtp->add(rtnvscp->varp()->name() + tmpSuffixp + sel);  // LHS
                cstmtp->add(" = ");
                cstmtp->add(callp);  // RHS
                cstmtp->add(";");
                cfuncp->addStmtsp(cstmtp);
            } else {
                // Othervise just call it
                cfuncp->addStmtsp(callp->makeStmt());
            }
        }

        // Convert output/inout arguments back to internal type
        for (AstNode* stmtp = cfuncp->argsp(); stmtp; stmtp = stmtp->nextp()) {
            if (AstVar* const portp = VN_CAST(stmtp, Var)) {
                portp->protect(false);  // No additional exposure - already part of shown proto
                if (portp->isIO() && (portp->isWritable() || portp->isFuncReturn())
                    && !portp->isDpiOpenArray()) {
                    AstVarScope* const portvscp = VN_AS(
                        portp->user2p(), VarScope);  // Remembered when we created it earlier
                    cfuncp->addStmtsp(
                        createAssignDpiToInternal(portvscp, portp->name() + tmpSuffixp));
                }
            }
        }

        if (v3Global.opt.profExec()) {
            cfuncp->addStmtsp(AstCStmt::profExecSectionPop(nodep->fileline(), "dpiimports"));
        }
    }

    AstVarScope* getDpiExporTrigger() {
        AstNetlist* const netlistp = v3Global.rootp();
        AstVarScope* dpiExportTriggerp = netlistp->dpiExportTriggerp();
        if (!dpiExportTriggerp) {
            // Create the global DPI export trigger flag the first time we encounter a DPI export.
            // This flag is set any time a DPI export is invoked, and cleared at the end of eval.
            FileLine* const fl = m_topScopep->fileline();
            const string name{"__Vdpi_export_trigger"};
            AstVar* const varp = new AstVar{fl, VVarType::VAR, name, VFlagBitPacked{}, 1};
            m_topScopep->scopep()->modp()->addStmtsp(varp);
            dpiExportTriggerp = new AstVarScope{fl, m_topScopep->scopep(), varp};
            m_topScopep->scopep()->addVarsp(dpiExportTriggerp);
            netlistp->dpiExportTriggerp(dpiExportTriggerp);
        }
        return dpiExportTriggerp;
    }

    AstCFunc* makeUserFunc(AstNodeFTask* nodep, bool ftaskNoInline) {
        // Given a already cloned node, make a public C function, or a non-inline C function
        // Probably some of this work should be done later, but...
        // should the type of the function be bool/uint32/64 etc (based on lookup) or IData?
        AstNode::user2ClearTree();
        AstVar* rtnvarp = nullptr;
        if (nodep->isFunction()) {
            AstVar* const portp = VN_AS(nodep->fvarp(), Var);
            UASSERT_OBJ(portp, nodep, "function without function output variable");
            UASSERT_OBJ(portp->isFuncReturn(), nodep, "Not marked as function return var");
            if (nodep->dpiImport() || nodep->dpiExport()) {
                AstBasicDType* const bdtypep = portp->dtypep()->basicp();
                if (!bdtypep->isDpiPrimitive()) {
                    if (bdtypep->isDpiBitVec() && portp->width() > 32) {
                        portp->v3error("DPI function may not return a > 32 bits wide type "
                                       "other than basic types.\n"
                                       + portp->warnMore()
                                       + "... Suggest make it an output argument instead?");
                    }
                    if (bdtypep->isDpiLogicVec()) {
                        portp->v3error("DPI function may not return a 4-state type "
                                       "other than a single 'logic' (IEEE 1800-2023 35.5.5)");
                    }
                }
            } else if (nodep->taskPublic()) {
                if (portp->isWide()) {
                    nodep->v3warn(E_UNSUPPORTED,
                                  "Unsupported: Public functions with return > 64 bits wide.\n"
                                      + nodep->warnMore()
                                      + "... Suggest make it an output argument instead?");
                }
            }

            if (ftaskNoInline || nodep->dpiExport()) {
                portp->funcReturn(false);  // Converting return to 'outputs'
            }
            unlinkAndClone(nodep, portp, false);
            rtnvarp = portp;
            rtnvarp->funcLocal(true);
            rtnvarp->noCReset(true);  // As made for port in V3LinkResolve
            rtnvarp->name(rtnvarp->name()
                          + "__Vfuncrtn");  // Avoid conflict with DPI function name
            if (nodep->dpiImport() || nodep->dpiExport()) rtnvarp->protect(false);
        }

        // Create/pick up the DPI CFunc for DPI Import/ DPI Export.
        AstCFunc* dpiFuncp = nullptr;
        if (nodep->dpiImport() || nodep->dpiExport()) {
            UASSERT_OBJ(!(nodep->dpiOpenParent() && nodep->dpiOpenChild()), nodep,
                        "DPI task should not be both parent and child");
            dpiFuncp = getDpiFunc(nodep, rtnvarp);
            if (!dpiFuncp || (nodep->dpiImport() && nodep->dpiOpenParent())) {
                // No need to make more than just the DPI Import prototype, the children will
                // create the wrapper implementations.
                if (rtnvarp) VL_DO_DANGLING(pushDeletep(rtnvarp), rtnvarp);
                VL_DO_DANGLING(pushDeletep(nodep), nodep);
                return nullptr;
            }
        }

        AstVarScope* rtnvscp = nullptr;
        if (rtnvarp) {
            rtnvscp = new AstVarScope{rtnvarp->fileline(), m_scopep, rtnvarp};
            m_scopep->addVarsp(rtnvscp);
            rtnvarp->user2p(rtnvscp);
        }

        string prefix;
        if (nodep->dpiImport()) {
            prefix = "__Vdpiimwrap_";
        } else if (nodep->dpiExport()) {
            prefix = "__Vdpiexp_";
        } else if (ftaskNoInline) {
            prefix = "__VnoInFunc_";
        }
        // Unless public, v3Descope will not uniquify function names even if duplicate per-scope,
        // so make it unique now.
        string suffix;  // So, make them unique
        if (!nodep->taskPublic() && !nodep->classMethod()) suffix = "_" + m_scopep->nameDotless();
        const string name = ((nodep->name() == "new") ? "new" : prefix + nodep->name() + suffix);
        AstCFunc* const cfuncp = new AstCFunc{
            nodep->fileline(), name, m_scopep,
            ((nodep->taskPublic() && rtnvarp) ? rtnvarp->cPubArgType(true, true) : "")};
        // It's ok to combine imports because this is just a wrapper;
        // duplicate wrappers can get merged.
        cfuncp->dontCombine(!nodep->dpiImport());
        cfuncp->entryPoint(!nodep->dpiImport());
        cfuncp->funcPublic(nodep->taskPublic());
        cfuncp->dpiContext(nodep->dpiContext());
        cfuncp->dpiExportImpl(nodep->dpiExport());
        cfuncp->dpiImportWrapper(nodep->dpiImport());
        cfuncp->recursive(nodep->recursive());
        if (nodep->dpiImport() || nodep->dpiExport()) {
            cfuncp->isStatic(true);
            cfuncp->isLoose(true);
        } else {
            cfuncp->isStatic(false);
        }
        cfuncp->isVirtual(nodep->isVirtual());
        cfuncp->dpiPure(nodep->dpiPure());
        if (nodep->name() == "new") cfuncp->isConstructor(true);
        if (cfuncp->dpiExportImpl()) cfuncp->cname(nodep->cname());

        if (cfuncp->dpiImportWrapper()) cfuncp->cname(nodep->cname());

        const bool needSyms
            = (!nodep->dpiImport() && !nodep->taskPublic()) || v3Global.opt.profExec();
        if (needSyms) cfuncp->argTypes(EmitCUtil::symClassVar());

        if (!nodep->dpiImport() && !nodep->taskPublic()) {
            // Need symbol table
            if (cfuncp->isConstructor()) {
                bool isInterfaceClass = false;
                if (const AstClass* const classp = VN_CAST(m_modp, Class)) {
                    isInterfaceClass = classp->isInterfaceClass();
                }
                if (!isInterfaceClass) {
                    const string stmt = VIdProtect::protect("_ctor_var_reset") + "(vlSymsp);";
                    cfuncp->addStmtsp(
                        new AstCStmt{nodep->fileline(), stmt, VCStmtType::CTOR_VAR_RESET_CALL});
                }
            }
        }
        if (nodep->dpiContext()) {
            // First three args go to dpiContext call
            createInputVar(cfuncp, "__Vscopep", VBasicDTypeKwd::SCOPEPTR);
            createInputVar(cfuncp, "__Vfilenamep", VBasicDTypeKwd::CHARPTR);
            createInputVar(cfuncp, "__Vlineno", VBasicDTypeKwd::INT);
        }

        if (nodep->dpiExport()) {
            AstScopeName* const snp = nodep->scopeNamep();
            UASSERT_OBJ(snp, nodep, "Missing scoping context");
            // The AstScopeName is really a statement(ish) for tracking, not a function
            snp->dpiExport(true);
            snp->unlinkFrBack();
            cfuncp->addStmtsp(snp);
        }

        // Create list of arguments and move to function
        for (AstNode *nextp, *stmtp = nodep->stmtsp(); stmtp; stmtp = nextp) {
            nextp = stmtp->nextp();
            if (AstVar* const portp = VN_CAST(stmtp, Var)) {
                if (portp->isParam() && VN_IS(portp->valuep(), InitArray)) {
                    // Move array parameters in functions into constant pool
                    portp->unlinkFrBack();
                    pushDeletep(portp);
                    AstNode* const tablep = v3Global.rootp()->constPoolp()->findTable(
                        VN_AS(portp->valuep(), InitArray));
                    portp->user2p(tablep);
                } else {
                    if (portp->isIO()) {
                        // Move it to new function
                        unlinkAndClone(nodep, portp, false);
                        portp->funcLocal(true);
                        cfuncp->addArgsp(portp);
                    } else {
                        // "Normal" variable, mark inside function
                        portp->funcLocal(true);
                    }
                    AstVarScope* const newvscp
                        = new AstVarScope{portp->fileline(), m_scopep, portp};
                    m_scopep->addVarsp(newvscp);
                    portp->user2p(newvscp);
                }
            }
        }

        // Fake output variable if was a function.  It's more efficient to
        // have it last, rather than first, as the C compiler can sometimes
        // avoid copying variables when calling shells if argument 1
        // remains argument 1 (which it wouldn't if a return got added).
        if (rtnvarp) cfuncp->addArgsp(rtnvarp);

        // Move body
        AstNode* bodysp = nodep->stmtsp();
        if (bodysp) {
            unlinkAndClone(nodep, bodysp, true);
            AstBegin* const tempp
                = new AstBegin{nodep->fileline(), "[EditWrapper]", bodysp, false};
            VL_DANGLING(bodysp);
            // If we cloned due to recursion, now need to rip out the ports
            // (that remained in place) then got cloned
            for (AstNode *nextp, *stmtp = tempp->stmtsp(); stmtp; stmtp = nextp) {
                nextp = stmtp->nextp();
                if (AstVar* const portp = VN_CAST(stmtp, Var)) {
                    if (portp->isIO()) VL_DO_DANGLING(pushDeletep(portp->unlinkFrBack()), portp);
                }
            }
            if (tempp->stmtsp()) cfuncp->addStmtsp(tempp->stmtsp()->unlinkFrBackWithNext());
            VL_DO_DANGLING(tempp->deleteTree(), tempp);
        }
        if (nodep->dpiImport()) bodyDpiImportFunc(nodep, rtnvscp, cfuncp, dpiFuncp);

        // Return statement
        if (rtnvscp && nodep->taskPublic()) {
            cfuncp->addStmtsp(new AstCReturn{
                rtnvscp->fileline(), new AstVarRef{rtnvscp->fileline(), rtnvscp, VAccess::READ}});
        }
        // Replace variable refs
        relink(cfuncp);

        if (cfuncp->dpiExportImpl()) {
            // Mark all non-local variables written by the DPI exported function as being updated
            // by DPI exports. This ensures correct ordering and change detection later.

            // Mark non-local variables written by the exported function
            bool writesNonLocals = false;
            cfuncp->foreach([&writesNonLocals](AstVarRef* refp) {
                AstVar* const varp = refp->varScopep()->varp();
                // We are ignoring function locals as they should not be referenced anywhere
                // outside the enclosing AstCFunc, hence they are irrelevant for code ordering.
                if (varp->isFuncLocal()) return;
                // Check if written
                if (refp->access().isWriteOrRW()) {
                    // Mark it as written by DPI export
                    varp->setWrittenByDpi();
                    // Remember we had some
                    writesNonLocals = true;
                }
                // Check if read
                if (refp->access().isReadOrRW()) {
                    // Mark it as read by DPI export
                    varp->setReadByDpi();
                }
            });

            // If this DPI export writes some non-local variables, set the DPI Export Trigger flag
            // in the function.
            if (writesNonLocals) {
                AstVarScope* const dpiExportTriggerp = getDpiExporTrigger();
                FileLine* const flp = cfuncp->fileline();

                // Set DPI export trigger flag every time the DPI export is called.
                AstAssign* const assignp
                    = new AstAssign{flp, new AstVarRef{flp, dpiExportTriggerp, VAccess::WRITE},
                                    new AstConst{flp, AstConst::BitTrue{}}};

                // Add as first statement (to avoid issues with early returns) to exported function
                if (cfuncp->stmtsp()) {
                    cfuncp->stmtsp()->addHereThisAsNext(assignp);
                } else {
                    cfuncp->addStmtsp(assignp);
                }
            }
        }

        // Mark the fact that this function allocates std::process
        if (nodep->needProcess()) cfuncp->setNeedProcess();

        // Delete rest of cloned task and return new func
        VL_DO_DANGLING(pushDeletep(nodep), nodep);
        UINFOTREE(9, cfuncp, "", "userFunc");
        return cfuncp;
    }

    void iterateIntoFTask(AstNodeFTask* nodep) {
        // Iterate into the FTask we are calling.  Note it may be under a different
        // scope then the caller, so we need to restore state.
        VL_RESTORER(m_scopep);
        VL_RESTORER(m_modp);
        VL_RESTORER(m_insStmtp);
        m_scopep = m_statep->getScope(nodep);
        m_modp = m_scopep->modp();
        iterate(nodep);
    }
    void insertBeforeStmt(AstNode* nodep, AstNode* newp) {
        UINFOTREE(9, nodep, "", "newstmt");
        UASSERT_OBJ(m_insStmtp, nodep, "Function call not underneath a statement");
        UINFOTREE(9, newp, "", "newfunc");
        m_insStmtp->addHereThisAsNext(newp);
    }
    void processArgs(AstNodeFTaskRef* nodep) {
        // Create a fresh variable for each concat array present in pins list
        for (AstArg* argp = nodep->argsp(); argp; argp = VN_AS(argp->nextp(), Arg)) {
            AstInitArray* const arrayp = VN_CAST(argp->exprp(), InitArray);
            if (!arrayp) continue;

            FileLine* const flp = arrayp->fileline();
            const std::string tempName = m_initArrayTmpNames.get(argp);
            AstVar* const substp = new AstVar{flp, VVarType::VAR, tempName, arrayp->dtypep()};
            substp->funcLocal(true);
            AstVarScope* const substvscp = createVarScope(substp, tempName);

            AstAssign* const assignp
                = new AstAssign{flp, new AstVarRef{arrayp->fileline(), substvscp, VAccess::WRITE},
                                arrayp->unlinkFrBack()};

            AstExprStmt* const exprstmtp = new AstExprStmt{
                flp, substp, new AstVarRef{arrayp->fileline(), substvscp, VAccess::READ}};
            exprstmtp->stmtsp()->addNext(assignp);
            argp->exprp(exprstmtp);
        }
    }

    bool isIfaceLocalImpure(AstNodeFTask* nodep, AstNode* impurep) {
        const AstScope* const scopep = m_statep->getScope(nodep);
        if (!TaskStateVisitor::isIfaceFTaskScope(scopep)) return false;

        const AstVarRef* const refp = VN_CAST(impurep, VarRef);
        if (!refp || !refp->varScopep()) return false;

        return refp->varScopep()->scopep() == scopep;
    }

    // VISITORS
    void visit(AstNodeModule* nodep) override {
        VL_RESTORER(m_modp);
        VL_RESTORER(m_modNCalls);
        m_modp = nodep;
        m_modNCalls = 0;
        iterateChildren(nodep);
        UASSERT_OBJ(!m_insStmtp, nodep, "Didn't finish out last statement");
    }
    void visit(AstWith* nodep) override {
        if (nodep->user1SetOnce()) {
            // Make sure that the return expression is converted only once
            return;
        }
        AstNodeExpr* const withExprp = VN_AS(nodep->exprp()->unlinkFrBack(), NodeExpr);
        nodep->addExprp(new AstCReturn{withExprp->fileline(), withExprp});
        iterateChildren(nodep);
    }
    void visit(AstScope* nodep) override {
        VL_RESTORER(m_scopep);
        m_scopep = nodep;
        iterateChildren(nodep);
        UASSERT_OBJ(!m_insStmtp, nodep, "Didn't finish out last statement");
    }
    void visit(AstCNew* nodep) override {
        VL_RESTORER(m_inNew);
        m_inNew = true;
        iterateChildren(nodep);
    }
    void visit(AstNodeFTaskRef* nodep) override {
        if (m_inSensesp && !nodep->isPure()) {
            nodep->v3warn(E_UNSUPPORTED,
                          "Unsupported: Impure function calls in sensitivity lists");
            nodep->taskp(nullptr);  // So V3Broken doesn't complain
            return;
        }
        // Includes handling AstMethodCall, AstNew
        UASSERT_OBJ(nodep->taskp(), nodep, "Unlinked?");
        iterateIntoFTask(nodep->taskp());  // First, do hierarchical funcs
        UINFO(4, " FTask REF   " << nodep);
        UINFOTREE(9, nodep, "", "inlfunc");
        UASSERT_OBJ(m_scopep, nodep, "func ref not under scope");
        const string namePrefix = ((VN_IS(nodep, FuncRef) ? "__Vfunc_" : "__Vtask_")
                                   + nodep->taskp()->shortName() + "__" + cvtToStr(m_modNCalls++));
        // Create output variable
        AstVarScope* outvscp = nullptr;
        if (nodep->taskp()->isFunction()) {
            AstVar* const fvarp = VN_AS(nodep->taskp()->fvarp(), Var);
            // If the call is on the RHS of a simple assignment 'lhs = call()',
            // the LHS variable can be reused as the output variable iff it has
            // the same type, and the function does not read the output variable itself.
            // This can be proven cheaply if the LHS is an automatic variable and the
            // function does not have any ref arguments. This arises a lot after V3LiftExpr.
            if (AstAssign* const assignp = VN_CAST(nodep->backp(), Assign)) {
                if (AstVarRef* const lhsp = VN_CAST(assignp->lhsp(), VarRef)) {
                    AstVarScope* const vscp = lhsp->varScopep();
                    if (vscp->varp()->lifetime().isAutomatic()
                        && vscp->varp()->dtypep()->skipRefp()->sameTree(
                            fvarp->dtypep()->skipRefp())
                        && !hasRefArgument(nodep->taskp())) {
                        outvscp = vscp;
                    }
                }
            }
            // Otherwise create a new variable for the result
            if (!outvscp) outvscp = createVarScope(fvarp, namePrefix + "__Vfuncout");
        }
        // Create cloned statements
        AstNode* beginp;
        AstCNew* cnewp = nullptr;
        const bool virtualIfaceCall
            = TaskStateVisitor::isVirtualIfaceMethodCall(nodep)
              && TaskStateVisitor::isIfaceFTaskScope(m_statep->getScope(nodep->taskp()));
        if (m_statep->ftaskNoInline(nodep->taskp()) || virtualIfaceCall) {
            processArgs(nodep);
            // This may share VarScope's with a public task, if any.  Yuk.
            beginp = createNonInlinedFTask(nodep, namePrefix, outvscp, cnewp /*ref*/);
        } else {
            beginp = createInlinedFTask(nodep, namePrefix, outvscp);
            ++m_statInlines;
        }

        if (VN_IS(nodep, New)) {  // New not legal as while() condition
            insertBeforeStmt(nodep, beginp);
            UASSERT_OBJ(cnewp, nodep, "didn't create cnew for new");
            nodep->replaceWith(cnewp);
            VL_DO_DANGLING(nodep->deleteTree(), nodep);
        } else if (VN_IS(nodep->backp(), NodeAssign)) {
            UASSERT_OBJ(nodep->taskp()->isFunction(), nodep,
                        "funcref-like assign to non-function");
            insertBeforeStmt(nodep, beginp);
            AstVarRef* const outrefp = new AstVarRef{nodep->fileline(), outvscp, VAccess::READ};
            nodep->replaceWith(outrefp);
            VL_DO_DANGLING(nodep->deleteTree(), nodep);
        } else if (!VN_IS(nodep->backp(), StmtExpr)) {
            UASSERT_OBJ(nodep->taskp()->isFunction(), nodep,
                        "funcref-like expression to non-function");
            AstVarRef* const outrefp = new AstVarRef{nodep->fileline(), outvscp, VAccess::READ};
            AstExprStmt* lambdap = new AstExprStmt{nodep->fileline(), beginp, outrefp};

            if (m_inNew) {
                AstVar* varp = outvscp->varp();

                // Create a new var that will be inside the lambda
                AstVar* newvarp = varp->cloneTree(false);
                newvarp->funcLocal(true);

                // Create a new VarScope for the new variable
                AstVarScope* const newvscp
                    = new AstVarScope{newvarp->fileline(), m_scopep, newvarp};
                m_scopep->addVarsp(newvscp);

                // Replace all references so they point to the new var and varscope
                lambdap->stmtsp()->foreachAndNext([outvscp, newvscp](AstVarRef* refp) {
                    if (refp->varScopep() == outvscp) {
                        refp->varScopep(newvscp);
                        refp->varp(newvscp->varp());
                    }
                });

                // Add variable initialization
                lambdap->stmtsp()->addHereThisAsNext(newvarp);
                lambdap->hasResult(false);

                // Add return statement - TODO: Why not VarRef(outvscp)?
                AstCExpr* const exprp = new AstCExpr{nodep->fileline(), varp->name()};
                exprp->dtypeSetString();
                lambdap->addStmtsp(new AstCReturn{nodep->fileline(), exprp});
            }

            // AstExprStmt is currently treated as impure, so clear the cached purity of its
            // parents
            nodep->replaceWith(lambdap);
            VL_DO_DANGLING(nodep->deleteTree(), nodep);
            VIsCached::clearCacheTree();
        } else {  // VN_IS(nodep->backp(), StmtExpr)
            insertBeforeStmt(nodep, beginp);
            if (nodep->taskp()->isFunction()
                && !nodep->backp()->fileline()->warnIsOff(V3ErrorCode::IGNOREDRETURN)) {
                nodep->v3warn(
                    IGNOREDRETURN,
                    "Ignoring return value of non-void function (IEEE 1800-2023 13.4.1)");
            }
            nodep->unlinkFrBack();
            VL_DO_DANGLING(nodep->deleteTree(), nodep);
        }
        UINFO(4, "  FTask REF Done.");
    }
    void visit(AstNodeFTask* const nodep) override {
        UINFO(4, " visitFTask   " << nodep);
        VL_RESTORER(m_insStmtp);
        m_insStmtp = nodep->stmtsp();  // Might be null if no statements, but we won't use it
        if (!nodep->user1SetOnce()) {  // Just one creation needed per function
            // Expand functions in it
            int modes = 0;
            if (nodep->dpiImport()) modes++;
            if (nodep->dpiExport()) modes++;
            if (nodep->taskPublic()) modes++;
            if (nodep->classMethod()) modes++;
            if (v3Global.opt.protectIds() && nodep->taskPublic()) {
                // We always call protect() on names, we don't check if public or not
                // Hence any external references wouldn't be able to find the refed public object.
                nodep->v3warn(E_UNSUPPORTED,
                              "Unsupported: Using --protect-ids with public function");
            }
            if (modes > 1) {
                nodep->v3error("Cannot mix DPI import, DPI export, class methods, and/or public "
                               "on same function: "
                               << nodep->prettyNameQ());
            }

            const bool noInline = m_statep->ftaskNoInline(nodep);
            const bool needsNonInlineCFunc = m_statep->ftaskNeedsNonInlineCFunc(nodep);
            // Warn if not inlining an impure ftask (unless method, recursive,
            // or interface-local member access, which is not truly external).
            // Will likely not schedule correctly.
            // TODO: Why not if recursive? It will not work ...
            if (noInline && !nodep->classMethod() && !nodep->recursive()) {
                if (AstNode* const impurep = m_statep->checkImpure(nodep)) {
                    if (!isIfaceLocalImpure(nodep, impurep)) {
                        nodep->v3warn(IMPURE, "Unsupported: External variable referenced by "
                                              "non-inlined function/task: "
                                                  << nodep->prettyNameQ() << '\n'
                                                  << nodep->warnContextPrimary() << '\n'
                                                  << impurep->warnOther()
                                                  << "... Location of the external reference: "
                                                  << impurep->prettyNameQ() << '\n'
                                                  << impurep->warnContextSecondary());
                    }
                }
            }

            if (noInline || needsNonInlineCFunc || nodep->dpiImport() || nodep->dpiExport()
                || nodep->taskPublic()) {
                // Clone it first, because we may have later FTaskRef's that still need
                // the original version.
                AstNodeFTask* const clonedFuncp = nodep->cloneTree(false);
                if (nodep->isConstructor()) m_statep->remapFuncClassp(nodep, clonedFuncp);
                const bool makeNonInlineFunc = noInline || needsNonInlineCFunc;
                if (AstCFunc* const cfuncp = makeUserFunc(clonedFuncp, makeNonInlineFunc)) {
                    nodep->addNextHere(cfuncp);
                    if (nodep->dpiImport() || makeNonInlineFunc) {
                        m_statep->ftaskCFuncp(nodep, cfuncp);
                    }
                    iterateIntoFTask(clonedFuncp);  // Do the clone too
                }
            }

            // Any variables inside the function still have varscopes pointing to them.
            // We're going to delete the vars, so delete the varscopes.
            if (nodep->isFunction()) {
                if (AstVar* const portp = VN_CAST(nodep->fvarp(), Var)) {
                    AstVarScope* const vscp = m_statep->findVarScope(m_scopep, portp);
                    UINFO(9, "   funcremovevsc " << vscp);
                    VL_DO_DANGLING(pushDeletep(vscp->unlinkFrBack()), vscp);
                }
            }
            for (AstNode *nextp, *stmtp = nodep->stmtsp(); stmtp; stmtp = nextp) {
                nextp = stmtp->nextp();
                if (AstVar* const portp = VN_CAST(stmtp, Var)) {
                    AstVarScope* const vscp = m_statep->findVarScope(m_scopep, portp);
                    UINFO(9, "   funcremovevsc " << vscp);
                    VL_DO_DANGLING(pushDeletep(vscp->unlinkFrBack()), vscp);
                }
            }
            // Just push for deletion, as other references to func may
            // remain until visitor exits
            nodep->unlinkFrBack();
            VL_DO_DANGLING(pushDeletep(nodep), nodep);
        }
    }
    void visit(AstAssignForce* nodep) override {
        // Force statements cannot be converted to always blocks outside of a logic block
        // This causes function calls on RHS of force assignments to be improperly inlined and
        // called just once. To prevent this, we create a temporary variable for each function
        // reference on RHS. This variable is declared in the nearest scope and gets a continuous
        // assignment of the function, so it can be converted to always and properly inlined This
        // temporary variable becomes the RHS of the force assignment
        std::vector<AstNodeFTaskRef*> refs;
        nodep->rhsp()->foreach([&refs](AstNodeFTaskRef* refp) { refs.push_back(refp); });
        for (AstNodeFTaskRef* const refp : refs) {

            // Create the temporary variable and its scope
            // Replicate the logic from V3Task, every function call gets
            // a unique temp variable
            AstVar* const interVarp = new AstVar{
                nodep->fileline(), VVarType::VAR,
                refp->name() + "__Vforcefuncout" + m_forceTmpNames.get(nodep), refp->dtypep()};
            UASSERT_OBJ(m_modp->stmtsp(), m_modp, "Module should have statements in it");
            m_modp->stmtsp()->addHereThisAsNext(interVarp);
            AstVarScope* const interVscp = new AstVarScope{refp->fileline(), m_scopep, interVarp};
            m_scopep->addVarsp(interVscp);

            // Recompute the helper in a combo block so any inlined function body stays
            // inside schedulable logic rather than spilling statements at module scope.
            AstAssign* const assignp = new AstAssign{
                nodep->fileline(), new AstVarRef{nodep->fileline(), interVscp, VAccess::WRITE},
                nodep->rhsp()->cloneTreePure(false)};
            AstSenTree* const senTreep = new AstSenTree{
                nodep->fileline(), new AstSenItem{nodep->fileline(), AstSenItem::Combo{}}};
            AstActive* const activep
                = new AstActive{nodep->fileline(), "force-func-update", senTreep};
            activep->senTreeStorep(activep->sentreep());
            activep->addStmtsp(
                new AstAlways{nodep->fileline(), VAlwaysKwd::ALWAYS, nullptr, assignp});
            m_scopep->addBlocksp(activep);

            // Replace RHS of force assignment with the temporary variable
            refp->replaceWith(new AstVarRef{nodep->fileline(), interVscp, VAccess::READ});
            VL_DO_DANGLING(refp->deleteTree(), refp);
        }
    }
    void visit(AstNodeForeach* nodep) override {  // LCOV_EXCL_LINE
        nodep->v3fatalSrc(
            "Foreach statements should have been converted to while statements in V3Begin.cpp");
    }
    void visit(AstNodeStmt* nodep) override {
        VL_RESTORER(m_insStmtp);
        m_insStmtp = nodep;
        iterateChildren(nodep);
    }
    void visit(AstStmtExpr* nodep) override {
        VL_RESTORER(m_insStmtp);
        m_insStmtp = nodep;
        iterateChildren(nodep);
        if (!nodep->exprp()) VL_DO_DANGLING(nodep->unlinkFrBack()->deleteTree(), nodep);
    }
    void visit(AstSenItem* nodep) override {
        UASSERT_OBJ(!m_inSensesp, nodep, "Senitem under senitem?");
        VL_RESTORER(m_inSensesp);
        m_inSensesp = true;
        iterateChildren(nodep);
    }
    //--------------------
    void visit(AstNode* nodep) override { iterateChildren(nodep); }

public:
    // CONSTRUCTORS
    TaskVisitor(AstNetlist* nodep, TaskStateVisitor* statep)
        : m_statep{statep}
        , m_initArrayTmpNames{"__VInitArrayTemp"} {
        iterate(nodep);
    }
    ~TaskVisitor() {
        V3Stats::addStat("Optimizations, Functions inlined", m_statInlines);
        V3Stats::addStat("Optimizations, Hierarchical DPI wrappers with costs",
                         m_statHierDpisWithCosts);
    }
};

//######################################################################
// Task class functions

const char* const V3Task::s_dpiTemporaryVarSuffix = "__Vcvt";

V3TaskConnects V3Task::taskConnects(AstNodeFTaskRef* nodep, AstNode* taskStmtsp,
                                    V3TaskConnectState* statep, bool makeChanges) {
    // Output list will be in order of the port declaration variables (so
    // func calls are made right in C)
    // Missing pin/expr?  We return (pinvar, nullptr)
    // Extra   pin/expr?  We clean it up
    UINFO(9, "taskConnects " << nodep);
    std::map<const std::string, int> nameToIndex;
    V3TaskConnects tconnects;
    UASSERT_OBJ(nodep->taskp(), nodep, "unlinked");

    // Find ports
    int tpinnum = 0;
    AstVar* sformatp = nullptr;
    for (AstNode* stmtp = taskStmtsp; stmtp; stmtp = stmtp->nextp()) {
        if (AstVar* const portp = VN_CAST(stmtp, Var)) {
            if (portp->isIO()) {
                tconnects.emplace_back(portp, static_cast<AstArg*>(nullptr));
                nameToIndex.emplace(portp->name(), tpinnum);  // For name based connections
                ++tpinnum;
                if (portp->attrSFormat()) {
                    sformatp = portp;
                } else if (sformatp) {
                    portp->v3error("/*verilator sformat*/ can only be applied to last argument of "
                                   "a function");
                }
            }
        }
    }

    // Find pins
    int ppinnum = 0;
    bool reorganize = false;
    for (AstArg *argp = nodep->argsp(), *nextp; argp; argp = nextp) {
        nextp = VN_AS(argp->nextp(), Arg);
        if (!argp->name().empty()) {
            // By name
            const auto it = nameToIndex.find(argp->name());
            if (it == nameToIndex.end()) {
                if (makeChanges) {
                    argp->v3error(
                        "No such argument "
                        << argp->prettyNameQ() << " in call to " << nodep->taskp()->verilogKwd()
                        << " " << nodep->taskp()->prettyNameQ() << '\n'
                        << nodep->warnContextPrimary() << '\n'
                        << nodep->warnMore() << "... Location of " << nodep->taskp()->verilogKwd()
                        << " " << nodep->taskp()->prettyNameQ() << " declaration\n"
                        << nodep->taskp()->warnContextSecondary());
                    // We'll just delete it; seems less error prone than making a false argument
                    VL_DO_DANGLING(argp->unlinkFrBack()->deleteTree(), argp);
                }
            } else {
                if (tconnects[it->second].second && makeChanges) {
                    argp->v3error("Duplicate argument " << argp->prettyNameQ()
                                                        << " in function call to "
                                                        << nodep->taskp()->prettyTypeName());
                    tconnects[it->second].second->unlinkFrBack()->deleteTree();
                }
                tconnects[it->second].second = argp;
                reorganize = true;
            }
        } else {  // By pin number
            if (nodep->taskp()->prettyName() == "randomize") {
                // Arguments to randomize() are special, will be handled in V3Randomize
            } else if (ppinnum >= tpinnum) {
                if (sformatp) {
                    tconnects.emplace_back(sformatp, static_cast<AstArg*>(nullptr));
                    tconnects[ppinnum].second = argp;
                    ++tpinnum;
                } else if (makeChanges) {
                    argp->v3error("Too many arguments in call to "
                                  << nodep->taskp()->verilogKwd() << " "
                                  << nodep->taskp()->prettyNameQ() << '\n'
                                  << nodep->warnContextPrimary() << '\n'
                                  << nodep->warnMore() << "... Location of "
                                  << nodep->taskp()->verilogKwd() << " "
                                  << nodep->taskp()->prettyNameQ() << " declaration:\n"
                                  << nodep->taskp()->warnContextSecondary());
                    // We'll just delete it; seems less error prone than making a false argument
                    VL_DO_DANGLING(argp->unlinkFrBack()->deleteTree(), argp);
                }
            } else {
                tconnects[ppinnum].second = argp;
            }
        }
        ++ppinnum;
    }

    if (!makeChanges) return tconnects;

    // Connect missing ones
    VInsertionSet<const AstVar*> argWrap;  // Which ports are defaulted; need arg wrapper creation
    for (int i = 0; i < tpinnum; ++i) {
        AstVar* const portp = tconnects[i].first;
        if (!tconnects[i].second || !tconnects[i].second->exprp()) {
            AstNodeExpr* newvaluep = nullptr;
            if (!portp->valuep()) {
                nodep->v3error("Missing argument on non-defaulted argument "
                               << portp->prettyNameQ() << " in function call to "
                               << nodep->taskp()->prettyTypeName());
                newvaluep = new AstConst{nodep->fileline(), AstConst::Unsized32{}, 0};
            } else if (AstFuncRef* const funcRefp = VN_CAST(portp->valuep(), FuncRef)) {
                const AstNodeFTask* const funcp = funcRefp->taskp();
                if (funcp->classMethod() && funcp->isStatic()) newvaluep = funcRefp;
            } else if (AstConst* const constp = VN_CAST(portp->valuep(), Const)) {
                newvaluep = constp;
            }

            if (!newvaluep) {
                // The default value for this port might be a constant
                // expression that hasn't been folded yet. Try folding it
                // now; we don't have much to lose if it fails.
                newvaluep = V3Const::constifyEdit(VN_AS(portp->valuep(), NodeExpr));
                if (!VN_IS(newvaluep, Const)) {
                    if (statep) {
                        portp->pinNum(i + 1);  // Make sure correct, will use to build name
                        UINFO(9, "taskConnects arg wrapper needed " << portp->valuep());
                        argWrap.insert(portp);
                    } else {  // statep = nullptr, called too late or otherwise to handle args
                        // Problem otherwise is we might have a varref, task
                        // call, or something else that only makes sense in the
                        // domain of the function (or class containing the method),
                        // versus that of the callee.
                        nodep->v3warn(
                            E_UNSUPPORTED,
                            "Unsupported: Non-constant default value in missing argument "
                                << portp->prettyNameQ() << " in function call to "
                                << nodep->taskp()->prettyTypeName());
                        newvaluep = new AstConst{nodep->fileline(), AstConst::Unsized32{}, 0};
                    }
                }
            }
            newvaluep = newvaluep->backp() ? newvaluep->cloneTree(true) : newvaluep;
            // To avoid problems with callee needing to know to deleteTree
            // or not, we make this into a pin
            UINFO(9, "Default pin for " << portp);
            AstArg* const newp = new AstArg{nodep->fileline(), portp->name(), newvaluep};
            if (tconnects[i].second) {  // Have a "nullptr" pin already defined for it
                VL_DO_CLEAR(tconnects[i].second->unlinkFrBack()->deleteTree(),
                            tconnects[i].second = nullptr);
            }
            tconnects[i].second = newp;
            reorganize = true;
        }
        if (tconnects[i].second) {
            UINFO(9, "Connect " << portp << "  ->  " << tconnects[i].second);
        } else {
            UINFO(9, "Connect " << portp << "  ->  NONE");
        }
    }

    for (const auto& tconnect : tconnects) {
        AstArg* const argp = tconnect.second;
        argp->name("");  // Can forget name as will add back in pin order
    }

    if (reorganize) {
        // To simplify downstream, put argument list back into pure pinnumber ordering
        while (nodep->argsp()) {
            // Must unlink each pin, not all pins linked together as one list
            nodep->argsp()->unlinkFrBack();
        }
        for (int i = 0; i < tpinnum; ++i) {
            AstArg* const argp = tconnects[i].second;
            UASSERT_OBJ(argp, nodep, "Lost argument in func conversion");
            nodep->addArgsp(argp);
        }
    }

    if (debug() >= 9) {  // LCOV_EXCL_START
        nodep->dumpTree("-  ftref-out: ");
        for (int i = 0; i < tpinnum; ++i) {
            UINFO(0, "   pin " << i << "  pin=" << cvtToHex(tconnects[i].first)
                               << "  conn=" << cvtToHex(tconnects[i].second));
        }
    }  // LCOV_EXCL_STOP

    if (!argWrap.empty()) {
        UINFO(9, "Arg wrapper generation " << nodep);
        // Create wrapper function with default argument settings.
        // Needed because the default needs symbol table of the called function.
        taskConnectWrap(nodep, tconnects, statep, argWrap);
        // Regenerate all connections, this time connecting to the wrapper
        return taskConnects(nodep, nodep->taskp()->stmtsp(),
                            // statep null, so can't recurse forever
                            nullptr);
    }
    return tconnects;
}

void V3Task::taskConnectWrap(AstNodeFTaskRef* nodep, const V3TaskConnects& tconnects,
                             V3TaskConnectState* statep,
                             const VInsertionSet<const AstVar*>& argWrap) {
    statep->setDidWrap();
    // Make wrapper name such that is same iff same args are defaulted
    std::string newname = nodep->name() + "__Vtcwrap";
    for (const AstVar* varp : argWrap) newname += "_" + cvtToStr(varp->pinNum());
    const auto pair = statep->wrapMap().emplace(std::piecewise_construct,
                                                std::forward_as_tuple(nodep->taskp(), newname),
                                                std::forward_as_tuple(nullptr));
    if (pair.second) {
        pair.first->second = taskConnectWrapNew(nodep->taskp(), newname, tconnects, argWrap);
    }
    AstNodeFTask* const newTaskp = pair.first->second;

    // Remove the defaulted arguments from original outside call
    for (const auto& tconnect : tconnects) {
        const AstVar* const portp = tconnect.first;
        AstArg* const argp = tconnect.second;
        if (argWrap.exists(portp)) {  // Removed arg
            statep->pushDeletep(argp->unlinkFrBack());
        }
    }
    // Change outside call to connect to new function
    nodep->taskp(newTaskp);
    nodep->name(newTaskp->name());
    // UINFOTREE(9, nodep, "", "taskConnectWrap-call");
}

AstNodeFTask* V3Task::taskConnectWrapNew(AstNodeFTask* taskp, const string& newname,
                                         const V3TaskConnects& tconnects,
                                         const VInsertionSet<const AstVar*>& argWrap) {
    std::map<const AstVar*, AstVar*> oldNewVars;  // Old -> new var mappings

    AstNodeFTask* const newTaskp = taskp->cloneType(newname);
    newTaskp->propagateAttrFrom(taskp);
    taskp->addNextHere(newTaskp);

    AstNodeFTaskRef* newCallp = nullptr;
    AstNode* newCallInsertp = nullptr;
    if (VN_IS(taskp, Func)) {
        AstVar* const fvarp = VN_AS(taskp->fvarp(), Var);
        UASSERT_OBJ(fvarp, taskp, "FuncRef without fvar");
        AstVar* const newFVarp = fvarp->cloneTree(true);
        oldNewVars.emplace(fvarp, newFVarp);
        newFVarp->name(newTaskp->name());
        newTaskp->fvarp(newFVarp);
        newTaskp->dtypeFrom(newFVarp);
        newCallp = new AstFuncRef{taskp->fileline(), VN_AS(taskp, Func)};
        newCallInsertp
            = new AstAssign{taskp->fileline(),
                            new AstVarRef{fvarp->fileline(), newFVarp, VAccess::WRITE}, newCallp};
        newCallInsertp->dtypeFrom(newFVarp);
    } else if (VN_IS(taskp, Task)) {
        newCallp = new AstTaskRef{taskp->fileline(), VN_AS(taskp, Task)};
        newCallInsertp = new AstStmtExpr{taskp->fileline(), newCallp};
    } else {
        taskp->v3fatalSrc("Unsupported: Non-constant default value in missing argument in a "
                          << taskp->prettyTypeName());
    }

    // Create wrapper's ports matching original's
    for (const auto& tconnect : tconnects) {
        AstVar* const portp = tconnect.first;
        AstVar* newPortp;
        if (!argWrap.exists(portp)) {  // Not removed arg
            newPortp = new AstVar{portp->fileline(), portp->varType(), portp->name(), portp};
            newPortp->propagateWrapAttrFrom(portp);
            newPortp->funcLocal(true);
            if (newPortp->valuep()) newPortp->valuep()->unlinkFrBack()->deleteTree();
            newTaskp->addStmtsp(newPortp);
        } else {  // Defaulting arg
            AstNodeExpr* const valuep = VN_AS(portp->valuep(), NodeExpr);
            // Create local temporary
            newPortp = new AstVar{portp->fileline(), VVarType::BLOCKTEMP, portp->name(),
                                  portp->dtypep()};
            newPortp->propagateAttrFrom(portp);
            newPortp->funcLocal(true);
            newTaskp->addStmtsp(newPortp);
            // Runtime-assign it to the default
            if (!VN_IS(valuep, EmptyQueue)) {
                AstAssign* const newAssignp
                    = new AstAssign{valuep->fileline(),
                                    new AstVarRef{valuep->fileline(), newPortp, VAccess::WRITE},
                                    valuep->cloneTree(true)};
                newTaskp->addStmtsp(newAssignp);
            }
        }
        oldNewVars.emplace(portp, newPortp);
        const VAccess pinAccess = portp->isWritable() ? VAccess::WRITE : VAccess::READ;
        AstArg* const newArgp = new AstArg{portp->fileline(), portp->name(),
                                           new AstVarRef{portp->fileline(), newPortp, pinAccess}};
        newCallp->addArgsp(newArgp);
    }
    // Create wrapper call to original, passing arguments, adding setting of return value
    newTaskp->addStmtsp(newCallInsertp);
    // Replace any varref's to original to new ports (e.g. in argument equations)
    newTaskp->foreach([=](AstVarRef* refp) {
        const auto it = oldNewVars.find(refp->varp());
        if (it != oldNewVars.end()) refp->varp(it->second);
    });
    // UINFOTREE(9, newTaskp, "", "taskConnectWrap-new");
    return newTaskp;
}

string V3Task::assignInternalToDpi(AstVar* portp, bool isPtr, const string& frSuffix,
                                   const string& toSuffix, const string& frPrefix) {
    // Create assignment from internal format into DPI temporary
    // Internal representation is scalar, 1D, or multi-dimensional array (similar to SV)
    // DPI temporary is scalar or 1D array (if unpacked array)
    string stmt;
    string ket;
    // Someday we'll have better type support, and this can make variables and casts.
    // But for now, we'll just text-bash it.
    const string frName = frPrefix + portp->name() + frSuffix;
    const string toName = portp->name() + toSuffix;
    const string idx = portp->name() + "__Vidx";
    size_t unpackSize = 1;  // non-unpacked array is treated as size 1
    int unpackDim = 0;
    if (AstUnpackArrayDType* const unpackp
        = VN_CAST(portp->dtypep()->skipRefp(), UnpackArrayDType)) {
        unpackSize = unpackp->arrayUnpackedElements();
        unpackDim = unpackp->dimensions(false).second;
        if (unpackDim > 0) UASSERT_OBJ(unpackSize > 0, portp, "size must be greater than 0");
    }
    if (portp->basicp()->isDpiBitVec() || portp->basicp()->isDpiLogicVec()) {
        const bool isBit = portp->basicp()->isDpiBitVec();
        const bool needsFor = unpackSize > 1;
        if (needsFor) {
            stmt = "for (size_t " + idx + " = 0; " + idx + " < " + cvtToStr(unpackSize) + "; ++"
                   + idx + ") ";
        }

        stmt += (isBit ? "VL_SET_SVBV_"s : "VL_SET_SVLV_"s)
                + portp->dtypep()->skipRefp()->charIQWN() + "(" + cvtToStr(portp->width()) + ", ";
        stmt += toName;
        if (needsFor) {
            stmt += " + " + cvtToStr(portp->dtypep()->skipRefp()->widthWords()) + " * " + idx;
        }
        stmt += ", ";
        if (unpackDim > 0) {  // Access multi-dimensional array as a 1D array
            stmt += "(&" + frName;
            for (int i = 0; i < unpackDim; ++i) stmt += "[0]";
            stmt += ")[" + (needsFor ? idx : "0") + "])";
        } else {
            stmt += frName + ")";
        }
    } else {
        const bool isChandle
            = portp->basicp() && portp->basicp()->keyword() == VBasicDTypeKwd::CHANDLE;
        const bool isString
            = portp->basicp() && portp->basicp()->keyword() == VBasicDTypeKwd::STRING;
        const string unpackAt = unpackSize > 1 ? "[" + idx + "]" : "[0]";
        if (unpackDim > 0) {
            const string forStmt = unpackSize > 1
                                       ? "for (size_t " + idx + " = 0; " + idx + " < "
                                             + cvtToStr(unpackSize) + "; ++" + idx + ") "
                                       : "";
            stmt += forStmt + toName + unpackAt;
        } else {
            if (isPtr) stmt += "*";  // DPI outputs are pointers
            stmt += toName;
        }
        stmt += " = ";
        if (isChandle) {
            stmt += "VL_CVT_Q_VP(";
            ket += ")";
        }
        if (unpackDim > 0) {
            stmt += "(&" + frName;
            for (int i = 0; i < unpackDim; ++i) stmt += "[0]";
            stmt += ")" + unpackAt;
        } else {
            stmt += frName;
        }
        if (isString) stmt += ".c_str()";
    }
    stmt += ket + ";";
    return stmt;
}

// Create assignment from DPI temporary into internal format
// DPI temporary is scalar or 1D array (if unpacked array)
// Internal representation is scalar, 1D, or multi-dimensional array (similar to SV)
std::string V3Task::assignDpiToInternal(const std::string& lhsName, AstVar* varp) {
    std::string cvt;
    bool useSvVec;
    std::tie(cvt, useSvVec) = TaskDpiUtils::dpiToInternalCvtStmt(varp);
    const std::vector<std::pair<int, int>> strides = TaskDpiUtils::unpackDimsAndStrides(varp);
    const int total = strides.empty() ? 1 : strides[0].first * strides[0].second;
    const int widthWords = varp->basicp()->widthWords();
    string statements;
    for (int i = 0; i < total; ++i) {
        std::string lhs = lhsName;
        // extract a scalar from multi-dimensional array (internal format)
        for (const auto& stride : strides) {
            lhs += "[" + std::to_string((i / stride.second) % stride.first) + "]";
        }
        // extract a scalar from DPI temporary var that is scalar or 1D array
        if (useSvVec) {
            const std::string offset = std::to_string(i * widthWords);
            statements += cvt + " " + lhs + ", " + varp->name() + " + " + offset + ");\n";
        } else {
            const std::string elem = strides.empty() ? "" : "[" + std::to_string(i) + "]";
            statements += lhs + " = " + cvt + varp->name() + elem + ")" + ";\n";
        }
    }
    return statements;
}

void V3Task::taskAll(AstNetlist* nodep) {
    UINFO(2, __FUNCTION__ << ":");
    {
        TaskStateVisitor visitors{nodep};
        const TaskVisitor visitor{nodep, &visitors};
    }  // Destruct before checking
    V3Global::dumpCheckGlobalTree("task", 0, dumpTreeEitherLevel() >= 3);
}
