// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Replace return/continue with jumps
//
// 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
//
//*************************************************************************
// V3LinkJump's Transformations:
//
// Each module:
//   Look for BEGINs
//      BEGIN(VAR...) -> VAR ... {renamed}
//   FOR -> WHILEs
//
//   Add JumpLabel which branches to after statements within JumpLabel
//      RETURN -> JUMPBLOCK(statements with RETURN changed to JUMPGO, ..., JUMPLABEL)
//      WHILE(... BREAK) -> JUMPBLOCK(WHILE(... statements with BREAK changed to JUMPGO),
//                                    ... JUMPLABEL)
//      WHILE(... CONTINUE) -> WHILE(JUMPBLOCK(... statements with CONTINUE changed to JUMPGO,
//                                    ... JUMPPABEL))
//
//*************************************************************************

#include "V3PchAstNoMT.h"  // VL_MT_DISABLED_CODE_UNIT

#include "V3LinkJump.h"

#include "V3AstUserAllocator.h"
#include "V3Error.h"
#include "V3UniqueNames.h"

#include <unordered_map>
#include <vector>

VL_DEFINE_DEBUG_FUNCTIONS;

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

class LinkJumpVisitor final : public VNVisitor {
    // NODE STATE
    //  AstBegin/etc::user1()  -> AstJumpBlock*, for body of this loop
    //  AstFinish::user1()     -> bool, processed
    //  AstNode::user2()       -> AstJumpBlock*, for this block
    //  AstNodeBegin::user3()  -> bool, true if contains a fork
    const VNUser1InUse m_user1InUse;
    const VNUser2InUse m_user2InUse;
    const VNUser3InUse m_user3InUse;

    // STATE
    AstNodeModule* m_modp = nullptr;  // Current module
    AstNodeFTask* m_ftaskp = nullptr;  // Current function/task
    AstNode* m_loopp = nullptr;  // Current loop
    AstRandSequence* m_randsequencep = nullptr;  // Current randsequence
    bool m_loopInc = false;  // In loop increment
    bool m_inFork = false;  // Under fork
    int m_modRepeatNum = 0;  // Repeat counter
    VOptionBool m_unrollFull;  // Pragma full, disable, or default unrolling
    std::vector<AstNodeBlock*> m_blockStack;  // All begin blocks above current node
    V3UniqueNames m_queueNames{
        "__VprocessQueue"};  // Names for queues needed for 'disable' handling
    std::unordered_map<const AstTask*, AstVar*> m_taskDisableQueues;  // Per-task process queues
    std::unordered_map<const AstBegin*, AstVar*> m_beginDisableQueues;  // Per-begin process queues
    std::unordered_map<const AstTask*, AstBegin*>
        m_taskDisableBegins;  // Per-task process wrappers
    std::unordered_map<const AstBegin*, AstBegin*>
        m_beginDisableBegins;  // Per-begin process wrappers

    // METHODS
    // Get (and create if necessary) the JumpBlock for this statement
    AstJumpBlock* getJumpBlock(AstNode* nodep, bool endOfIter) {
        // Wrap 'nodep' in JumpBlock. If loop, wrap the body instead if endOfIter is true
        UINFO(4, "Create JumpBlock for " << nodep);

        // Made it previously?  We always jump to the end, so this works out
        if (endOfIter) {
            if (nodep->user1p()) return VN_AS(nodep->user1p(), JumpBlock);
        } else {
            if (nodep->user2p()) return VN_AS(nodep->user2p(), JumpBlock);
        }

        AstNode* underp = nullptr;
        bool under_and_next = true;
        if (AstBegin* const blockp = VN_CAST(nodep, Begin)) {
            UASSERT_OBJ(!endOfIter, nodep, "No endOfIter for Begin");
            underp = blockp->stmtsp();
        } else if (AstNodeFTask* const fTaskp = VN_CAST(nodep, NodeFTask)) {
            UASSERT_OBJ(!endOfIter, nodep, "No endOfIter for FTask");
            underp = fTaskp->stmtsp();
        } else if (AstForeach* const foreachp = VN_CAST(nodep, Foreach)) {
            if (endOfIter) {
                underp = foreachp->bodyp();
                // Keep a LoopTest **at the front** outside the jump block
                if (VN_IS(underp, LoopTest)) underp = underp->nextp();
            } else {
                underp = nodep;
                under_and_next = false;  // IE we skip the entire foreach
            }
        } else if (AstLoop* const loopp = VN_CAST(nodep, Loop)) {
            if (endOfIter) {
                underp = loopp->stmtsp();
            } else {
                underp = nodep;
                under_and_next = false;  // IE we skip the entire loop
            }
        } else {
            nodep->v3fatalSrc("Unknown jump point for break/disable/continue");
            return nullptr;
        }
        // Skip over variables as we'll just move them in a moment
        // Also this would otherwise prevent us from using a label twice
        // see t_func_return test.
        while (underp && VN_IS(underp, Var)) underp = underp->nextp();
        UASSERT_OBJ(underp, nodep, "Break/disable/continue not under expected statement");
        UINFO(5, "  Underpoint is " << underp);

        // If already wrapped, we are done ...
        if (!underp->nextp() || !under_and_next) {
            if (AstJumpBlock* const blockp = VN_CAST(underp, JumpBlock)) return blockp;
        }

        // Move underp stuff to be under a new AstJumpBlock
        VNRelinker repHandle;
        if (under_and_next) {
            underp->unlinkFrBackWithNext(&repHandle);
        } else {
            underp->unlinkFrBack(&repHandle);
        }
        AstJumpBlock* const blockp = new AstJumpBlock{nodep->fileline(), underp};
        if (endOfIter) {
            nodep->user1p(blockp);
        } else {
            nodep->user2p(blockp);
        }
        repHandle.relink(blockp);

        // Keep any AstVars under the function not under the new JumpLabel
        for (AstNode *nextp, *varp = underp; varp; varp = nextp) {
            nextp = varp->nextp();
            if (VN_IS(varp, Var)) blockp->addHereThisAsNext(varp->unlinkFrBack());
        }
        return blockp;
    }
    void addPrefixToBlocksRecurse(const std::string& prefix, AstNode* const nodep) {
        // Add a prefix to blocks
        // Used to not have blocks with duplicated names
        if (AstBegin* const beginp = VN_CAST(nodep, Begin)) {
            if (beginp->name() != "") beginp->name(prefix + beginp->name());
        }

        if (AstNode* const refp = nodep->op1p()) addPrefixToBlocksRecurse(prefix, refp);
        if (AstNode* const refp = nodep->op2p()) addPrefixToBlocksRecurse(prefix, refp);
        if (AstNode* const refp = nodep->op3p()) addPrefixToBlocksRecurse(prefix, refp);
        if (AstNode* const refp = nodep->op4p()) addPrefixToBlocksRecurse(prefix, refp);
        if (AstNode* const refp = nodep->nextp()) addPrefixToBlocksRecurse(prefix, refp);
    }
    bool existsBlockAbove(const std::string& name) const {
        for (const AstNodeBlock* const stackp : vlstd::reverse_view(m_blockStack)) {
            if (stackp->name() == name) return true;
        }
        return false;
    }
    static AstStmtExpr* getQueuePushProcessSelfp(AstVarRef* const queueRefp) {
        // Constructs queue.push_back(std::process::self()) statement
        FileLine* const flp = queueRefp->fileline();
        return new AstStmtExpr{
            flp,
            new AstMethodCall{flp, queueRefp, "push_back",
                              new AstArg{flp, "", v3Global.rootp()->stdPackageProcessSelfp(flp)}}};
    }
    static AstStmtExpr* getQueuePushProcessSelfp(FileLine* const fl, AstVar* const processQueuep) {
        AstPackage* const topPkgp = v3Global.rootp()->dollarUnitPkgAddp();
        AstVarRef* const queueWriteRefp
            = new AstVarRef{fl, topPkgp, processQueuep, VAccess::WRITE};
        return getQueuePushProcessSelfp(queueWriteRefp);
    }
    static AstStmtExpr* getQueueKillStmtp(FileLine* const fl, AstVar* const processQueuep) {
        AstPackage* const topPkgp = v3Global.rootp()->dollarUnitPkgAddp();
        AstVarRef* const queueRefp = new AstVarRef{fl, topPkgp, processQueuep, VAccess::READWRITE};
        AstTaskRef* killQueueCall = nullptr;
        for (AstNode* itemp = v3Global.rootp()->stdPackageProcessp()->stmtsp(); itemp;
             itemp = itemp->nextp()) {
            if (itemp->name() == "killQueue") {
                killQueueCall
                    = new AstTaskRef{fl, VN_AS(itemp, Task), new AstArg{fl, "", queueRefp}};
                break;
            }
        }
        UASSERT(killQueueCall, "Should be found");
        killQueueCall->classOrPackagep(v3Global.rootp()->stdPackageProcessp());
        return new AstStmtExpr{fl, killQueueCall};
    }
    static void prependStmtsp(AstNodeFTask* const nodep, AstNode* const stmtp) {
        if (AstNode* const origStmtsp = nodep->stmtsp()) {
            origStmtsp->unlinkFrBackWithNext();
            stmtp->addNext(origStmtsp);
        }
        nodep->addStmtsp(stmtp);
    }
    static void prependStmtsp(AstNodeBlock* const nodep, AstNode* const stmtp) {
        if (AstNode* const origStmtsp = nodep->stmtsp()) {
            origStmtsp->unlinkFrBackWithNext();
            stmtp->addNext(origStmtsp);
        }
        nodep->addStmtsp(stmtp);
    }
    static bool directlyUnderFork(const AstNode* const nodep) {
        if (nodep->backp()->nextp() == nodep) return directlyUnderFork(nodep->backp());
        return VN_IS(nodep->backp(), Fork);
    }
    AstBegin* getOrCreateTaskDisableBeginp(AstTask* const taskp, FileLine* const fl) {
        const auto it = m_taskDisableBegins.find(taskp);
        if (it != m_taskDisableBegins.end()) return it->second;

        AstBegin* const taskBodyp = new AstBegin{fl, "", nullptr, false};
        // Disable-by-name rewrites kill this detached task-body process, so mark it as process
        // backed to ensure fork/join kill-accounting hooks are always emitted.
        taskBodyp->setNeedProcess();
        if (taskp->stmtsp()) taskBodyp->addStmtsp(taskp->stmtsp()->unlinkFrBackWithNext());

        AstFork* const forkp = new AstFork{fl, VJoinType::JOIN};
        forkp->addForksp(taskBodyp);
        taskp->addStmtsp(forkp);

        m_taskDisableBegins.emplace(taskp, taskBodyp);
        return taskBodyp;
    }
    AstVar* getProcessQueuep(AstNode* const nodep, FileLine* const fl) {
        AstPackage* const topPkgp = v3Global.rootp()->dollarUnitPkgAddp();
        AstVar* const processQueuep = new AstVar{
            fl, VVarType::VAR, m_queueNames.get(nodep->name()), VFlagChildDType{},
            new AstQueueDType{
                fl, VFlagChildDType{},
                new AstClassRefDType{fl, v3Global.rootp()->stdPackageProcessp(), nullptr},
                nullptr}};
        processQueuep->lifetime(VLifetime::STATIC_EXPLICIT);
        processQueuep->processQueue(true);
        processQueuep->setIgnoreSchedWrite();
        topPkgp->addStmtsp(processQueuep);
        return processQueuep;
    }
    AstVar* getOrCreateTaskDisableQueuep(AstTask* const taskp, FileLine* const fl) {
        const auto it = m_taskDisableQueues.find(taskp);
        if (it != m_taskDisableQueues.end()) return it->second;

        AstVar* const processQueuep = getProcessQueuep(taskp, fl);
        AstStmtExpr* const pushCurrentProcessp = getQueuePushProcessSelfp(fl, processQueuep);
        AstBegin* const taskBodyp = getOrCreateTaskDisableBeginp(taskp, fl);
        prependStmtsp(taskBodyp, pushCurrentProcessp);
        m_taskDisableQueues.emplace(taskp, processQueuep);
        return processQueuep;
    }
    AstBegin* getOrCreateBeginDisableBeginp(AstBegin* const beginp, FileLine* const fl) {
        const auto it = m_beginDisableBegins.find(beginp);
        if (it != m_beginDisableBegins.end()) return it->second;

        AstBegin* const beginBodyp = new AstBegin{fl, "", nullptr, false};
        // Disable-by-name rewrites kill this detached block-body process, so mark it as process
        // backed to ensure fork/join kill-accounting hooks are always emitted.
        beginBodyp->setNeedProcess();
        if (beginp->stmtsp()) beginBodyp->addStmtsp(beginp->stmtsp()->unlinkFrBackWithNext());

        AstFork* const forkp = new AstFork{fl, VJoinType::JOIN};
        forkp->addForksp(beginBodyp);
        beginp->addStmtsp(forkp);

        m_beginDisableBegins.emplace(beginp, beginBodyp);
        return beginBodyp;
    }
    AstVar* getOrCreateBeginDisableQueuep(AstBegin* const beginp, FileLine* const fl) {
        const auto it = m_beginDisableQueues.find(beginp);
        if (it != m_beginDisableQueues.end()) return it->second;

        AstVar* const processQueuep = getProcessQueuep(beginp, fl);
        AstStmtExpr* const pushCurrentProcessp = getQueuePushProcessSelfp(fl, processQueuep);
        AstBegin* const beginBodyp = getOrCreateBeginDisableBeginp(beginp, fl);
        prependStmtsp(beginBodyp, pushCurrentProcessp);

        // Named-block disable must also terminate detached descendants created by forks
        // under the block, so track each fork branch process in the same queue.
        beginBodyp->foreach([&](AstFork* const forkp) {
            for (AstBegin* branchp = forkp->forksp(); branchp;
                 branchp = VN_AS(branchp->nextp(), Begin)) {
                AstStmtExpr* const pushBranchProcessp
                    = getQueuePushProcessSelfp(fl, processQueuep);
                prependStmtsp(branchp, pushBranchProcessp);
            }
        });
        m_beginDisableQueues.emplace(beginp, processQueuep);
        return processQueuep;
    }
    void handleDisableOnFork(AstDisable* const nodep, const std::vector<AstBegin*>& forks) {
        // The support utilizes the process::kill()` method. For each `disable` a queue of
        // processes is declared. At the beginning of each fork that can be disabled, its process
        // handle is pushed to the queue. `disable` statement is replaced with calling `kill()`
        // method on each element of the queue.
        FileLine* const fl = nodep->fileline();
        AstNode* const targetp = nodep->targetp();
        if (m_ftaskp) {
            if (!m_ftaskp->exists(
                    [targetp](const AstNodeBlock* blockp) -> bool { return blockp == targetp; })) {
                // Disabling a fork, which is within the same task, is not a problem
                nodep->v3warn(E_UNSUPPORTED, "Unsupported: disabling fork from task / function");
            }
        }

        AstPackage* const topPkgp = v3Global.rootp()->dollarUnitPkgAddp();
        AstVar* const processQueuep = getProcessQueuep(targetp, fl);
        AstVarRef* const queueWriteRefp
            = new AstVarRef{fl, topPkgp, processQueuep, VAccess::WRITE};
        AstStmtExpr* pushCurrentProcessp = getQueuePushProcessSelfp(queueWriteRefp);

        for (AstBegin* const beginp : forks) {
            if (pushCurrentProcessp->backp()) {
                pushCurrentProcessp = pushCurrentProcessp->cloneTree(false);
            }
            prependStmtsp(beginp, pushCurrentProcessp);
        }
        AstStmtExpr* const killStmtp = getQueueKillStmtp(fl, processQueuep);
        nodep->addNextHere(killStmtp);

        // 'process::kill' does not immediately kill the current process
        // executing the disable statement (because it's in the running state).
        // If the disable statement is indeed executed by a process under the
        // target AstFork, then jump to the end of that fork branch.
        if (VN_IS(targetp, Fork)) {
            AstNodeBlock* forkBranchp = nullptr;
            for (AstNodeBlock* const blockp : vlstd::reverse_view(m_blockStack)) {
                if (blockp == targetp) {
                    AstJumpBlock* const jmpBlockp = getJumpBlock(VN_AS(forkBranchp, Begin), false);
                    killStmtp->addNextHere(new AstJumpGo{fl, jmpBlockp});
                    break;
                }
                forkBranchp = blockp;
            }
        }
    }
    // VISITORS
    void visit(AstNodeModule* nodep) override {
        if (nodep->dead()) return;
        VL_RESTORER(m_modp);
        VL_RESTORER(m_modRepeatNum);
        m_modp = nodep;
        m_modRepeatNum = 0;
        iterateChildren(nodep);
    }
    void visit(AstNodeFTask* nodep) override {
        VL_RESTORER(m_ftaskp);
        m_ftaskp = nodep;
        iterateChildren(nodep);
    }
    void visit(AstBegin* nodep) override {
        UINFO(8, "  " << nodep);
        VL_RESTORER(m_unrollFull);
        m_blockStack.push_back(nodep);
        iterateChildren(nodep);
        m_blockStack.pop_back();
    }
    void visit(AstFork* nodep) override {
        UINFO(8, "  " << nodep);
        VL_RESTORER(m_unrollFull);
        VL_RESTORER(m_inFork);
        m_inFork = true;
        // Mark all upper blocks, can stop once see one set to avoid O(n^2)
        for (AstNodeBlock* const blockp : vlstd::reverse_view(m_blockStack)) {
            if (blockp->user3SetOnce()) break;
        }
        m_blockStack.push_back(nodep);
        iterateChildren(nodep);
        m_blockStack.pop_back();
    }
    void visit(AstStmtPragma* nodep) override {
        if (nodep->pragp()->pragType() == VPragmaType::UNROLL_DISABLE) {
            m_unrollFull = VOptionBool::OPT_FALSE;
            VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep);
        } else if (nodep->pragp()->pragType() == VPragmaType::UNROLL_FULL) {
            m_unrollFull = VOptionBool::OPT_TRUE;
            VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep);
        } else {
            iterateChildren(nodep);
        }
    }
    void visit(AstRandSequence* nodep) override {
        VL_RESTORER(m_randsequencep);
        m_randsequencep = nodep;
        iterateChildren(nodep);
    }
    void visit(AstRepeat* nodep) override {
        // So later optimizations don't need to deal with them,
        //    REPEAT(count,body) -> loop=count,WHILE(loop>0) { body, loop-- }
        // Note var can be signed or unsigned based on original number.
        AstNodeExpr* const countp = nodep->countp()->unlinkFrBackWithNext();
        const string name = "__Vrepeat"s + cvtToStr(m_modRepeatNum++);
        AstBegin* const beginp = new AstBegin{nodep->fileline(), "", nullptr, true};
        // Spec says value is integral, if negative is ignored
        AstVar* const varp
            = new AstVar{nodep->fileline(), VVarType::BLOCKTEMP, name, nodep->findIntDType()};
        varp->lifetime(VLifetime::AUTOMATIC_EXPLICIT);
        varp->usedLoopIdx(true);
        beginp->addStmtsp(varp);
        AstNode* initsp = new AstAssign{
            nodep->fileline(), new AstVarRef{nodep->fileline(), varp, VAccess::WRITE}, countp};
        AstNode* const decp = new AstAssign{
            nodep->fileline(), new AstVarRef{nodep->fileline(), varp, VAccess::WRITE},
            new AstSub{nodep->fileline(), new AstVarRef{nodep->fileline(), varp, VAccess::READ},
                       new AstConst{nodep->fileline(), 1}}};
        AstNodeExpr* const zerosp = new AstConst{nodep->fileline(), AstConst::Signed32{}, 0};
        AstNodeExpr* const condp = new AstGtS{
            nodep->fileline(), new AstVarRef{nodep->fileline(), varp, VAccess::READ}, zerosp};
        AstNode* const bodysp = nodep->stmtsp();
        if (bodysp) bodysp->unlinkFrBackWithNext();
        FileLine* const flp = nodep->fileline();
        AstLoop* const loopp = new AstLoop{flp};
        loopp->addStmtsp(new AstLoopTest{flp, loopp, condp});
        loopp->addStmtsp(bodysp);
        loopp->addContsp(decp);
        if (!m_unrollFull.isDefault()) loopp->unroll(m_unrollFull);
        m_unrollFull = VOptionBool::OPT_DEFAULT_FALSE;
        beginp->addStmtsp(initsp);
        beginp->addStmtsp(loopp);
        // Replacement AstBegin will be iterated next
        nodep->replaceWith(beginp);
        VL_DO_DANGLING(nodep->deleteTree(), nodep);
    }
    void visit(AstLoop* nodep) override {
        if (!m_unrollFull.isDefault()) nodep->unroll(m_unrollFull);
        if (m_modp->hasParameterList() || m_modp->hasGParam()) {
            nodep->fileline()->modifyWarnOff(V3ErrorCode::UNUSEDLOOP, true);
        }
        m_unrollFull = VOptionBool::OPT_DEFAULT_FALSE;
        VL_RESTORER(m_loopp);
        VL_RESTORER(m_loopInc);
        m_loopp = nodep;
        m_loopInc = false;
        iterateAndNextNull(nodep->stmtsp());
        m_loopInc = true;
        iterateAndNextNull(nodep->contsp());
        // Move contsp into stmtsp, no longer needed to keep separately
        if (nodep->contsp()) nodep->addStmtsp(nodep->contsp()->unlinkFrBackWithNext());
    }
    void visit(AstNodeForeach* nodep) override {
        VL_RESTORER(m_loopp);
        m_loopp = nodep;
        iterateAndNextNull(nodep->bodyp());
    }
    void visit(AstReturn* nodep) override {
        iterateChildren(nodep);
        const AstFunc* const funcp = VN_CAST(m_ftaskp, Func);
        if (m_randsequencep) {
            nodep->replaceWith(new AstRSReturn{nodep->fileline()});
            VL_DO_DANGLING(pushDeletep(nodep), nodep);
            return;
        } else if (m_inFork) {
            nodep->v3error("Return isn't legal under fork (IEEE 1800-2023 9.2.3)");
            VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep);
            return;
        } else if (!m_ftaskp) {
            nodep->v3error("Return isn't underneath a task or function");
        } else if (funcp && !nodep->lhsp() && !funcp->isConstructor()) {
            nodep->v3error("Return underneath a function should have return value");
        } else if (!funcp && nodep->lhsp()) {
            nodep->v3error("Return underneath a task shouldn't have return value");
        } else {
            if (funcp && nodep->lhsp()) {
                // Set output variable to return value
                nodep->addHereThisAsNext(new AstAssign{
                    nodep->fileline(),
                    new AstVarRef{nodep->fileline(), VN_AS(funcp->fvarp(), Var), VAccess::WRITE},
                    nodep->lhsp()->unlinkFrBackWithNext()});
            }
            // Jump to the end of the function call
            AstJumpBlock* const blockp = getJumpBlock(m_ftaskp, false);
            nodep->addHereThisAsNext(new AstJumpGo{nodep->fileline(), blockp});
        }
        nodep->unlinkFrBack();
        VL_DO_DANGLING(pushDeletep(nodep), nodep);
    }
    void visit(AstBreak* nodep) override {
        iterateChildren(nodep);
        if (!m_loopp && m_randsequencep) {
            nodep->replaceWith(new AstRSBreak{nodep->fileline()});
            VL_DO_DANGLING(pushDeletep(nodep), nodep);
            return;
        } else if (!m_loopp) {
            nodep->v3error("break isn't underneath a loop");
        } else {
            // Jump to the end of the loop
            AstJumpBlock* const blockp = getJumpBlock(m_loopp, false);
            nodep->addNextHere(new AstJumpGo{nodep->fileline(), blockp});
        }
        nodep->unlinkFrBack();
        VL_DO_DANGLING(pushDeletep(nodep), nodep);
    }
    void visit(AstContinue* nodep) override {
        iterateChildren(nodep);
        if (!m_loopp) {
            nodep->v3error("continue isn't underneath a loop");
        } else {
            // Jump to the end of this iteration
            // If a "for" loop then need to still do the post-loop increment
            AstJumpBlock* const blockp = getJumpBlock(m_loopp, true);
            nodep->addNextHere(new AstJumpGo{nodep->fileline(), blockp});
        }
        nodep->unlinkFrBack();
        VL_DO_DANGLING(pushDeletep(nodep), nodep);
    }
    void visit(AstDisable* nodep) override {
        UINFO(8, "   DISABLE " << nodep);
        AstNode* const targetp = nodep->targetp();
        if (!targetp) {
            // Linking errors on the disable target are already reported upstream.
            // Drop this node to avoid cascading into an internal assertion.
            VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep);
            return;
        }
        if (AstTask* const taskp = VN_CAST(targetp, Task)) {
            AstVar* const processQueuep = getOrCreateTaskDisableQueuep(taskp, nodep->fileline());
            AstStmtExpr* const killStmtp = getQueueKillStmtp(nodep->fileline(), processQueuep);
            nodep->addNextHere(killStmtp);

            // process::kill does not terminate the currently running process immediately.
            // If we disable the current task by name from inside itself, jump to its end.
            if (m_ftaskp == taskp) {
                AstNode* jumpTargetp = taskp;
                const auto it = m_taskDisableBegins.find(taskp);
                if (it != m_taskDisableBegins.end()) jumpTargetp = it->second;
                AstJumpBlock* const blockp = getJumpBlock(jumpTargetp, false);
                killStmtp->addNextHere(new AstJumpGo{nodep->fileline(), blockp});
            }
        } else if (AstFork* const forkp = VN_CAST(targetp, Fork)) {
            std::vector<AstBegin*> forks;
            for (AstBegin* itemp = forkp->forksp(); itemp; itemp = VN_AS(itemp->nextp(), Begin)) {
                forks.push_back(itemp);
            }
            handleDisableOnFork(nodep, forks);
        } else if (AstBegin* const beginp = VN_CAST(targetp, Begin)) {
            if (existsBlockAbove(beginp->name())) {
                if (!beginp->user3()) {
                    // Jump to the end of the named block
                    AstJumpBlock* const blockp = getJumpBlock(beginp, false);
                    nodep->addNextHere(new AstJumpGo{nodep->fileline(), blockp});
                } else {
                    AstVar* const processQueuep
                        = getOrCreateBeginDisableQueuep(beginp, nodep->fileline());
                    AstStmtExpr* const killStmtp
                        = getQueueKillStmtp(nodep->fileline(), processQueuep);
                    nodep->addNextHere(killStmtp);

                    // process::kill does not terminate the currently running process immediately.
                    // If disable executes inside a fork branch of this named block, jump to the
                    // end of that branch to prevent statements after disable from executing.
                    AstBegin* currentBeginp = nullptr;
                    for (AstNodeBlock* const blockp : vlstd::reverse_view(m_blockStack)) {
                        if (VN_IS(blockp, Begin)) {
                            currentBeginp = VN_AS(blockp, Begin);
                            break;
                        }
                    }
                    if (currentBeginp && directlyUnderFork(currentBeginp)) {
                        AstJumpBlock* const blockp = getJumpBlock(currentBeginp, false);
                        killStmtp->addNextHere(new AstJumpGo{nodep->fileline(), blockp});
                    }
                }
            } else {
                AstVar* const processQueuep
                    = getOrCreateBeginDisableQueuep(beginp, nodep->fileline());
                AstStmtExpr* const killStmtp = getQueueKillStmtp(nodep->fileline(), processQueuep);
                nodep->addNextHere(killStmtp);
            }
        } else {
            nodep->v3fatalSrc("Disable linked with node of unhandled type "
                              << targetp->prettyTypeName());
        }
        nodep->unlinkFrBack();
        VL_DO_DANGLING(pushDeletep(nodep), nodep);
    }
    void visit(AstFinish* nodep) override {
        if (nodep->user1SetOnce()) return;  // Process once
        iterateChildren(nodep);
        if (m_inFork) {
            nodep->replaceWith(new AstFinishFork{nodep->fileline()});
            VL_DO_DANGLING(nodep->deleteTree(), nodep);
        } else if (m_loopp) {
            // Jump to the end of the loop (post-finish)
            AstJumpBlock* const blockp = getJumpBlock(m_loopp, false);
            nodep->addNextHere(new AstJumpGo{nodep->fileline(), blockp});
        }
    }
    void visit(AstVarRef* nodep) override {
        if (m_loopInc && nodep->varp()) nodep->varp()->usedLoopIdx(true);
    }
    void visit(AstConst*) override {}
    void visit(AstNode* nodep) override { iterateChildren(nodep); }

public:
    // CONSTRUCTORS
    explicit LinkJumpVisitor(AstNetlist* nodep) { iterate(nodep); }
    ~LinkJumpVisitor() override = default;
};

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

void V3LinkJump::linkJump(AstNetlist* nodep) {
    UINFO(2, __FUNCTION__ << ":");
    { LinkJumpVisitor{nodep}; }  // Destruct before checking
    V3Global::dumpCheckGlobalTree("linkjump", 0, dumpTreeEitherLevel() >= 3);
}
