473 lines
21 KiB
C++
473 lines
21 KiB
C++
// -*- mode: C++; c-file-style: "cc-mode" -*-
|
|
//*************************************************************************
|
|
// DESCRIPTION: Verilator: Replace return/continue with jumps
|
|
//
|
|
// Code available from: https://verilator.org
|
|
//
|
|
//*************************************************************************
|
|
//
|
|
// Copyright 2003-2025 by Wilson Snyder. 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-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 <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
|
|
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
|
|
|
|
// 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->stmtsp();
|
|
// 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);
|
|
}
|
|
static AstNode* getMemberp(const AstNodeModule* const nodep, const std::string& name) {
|
|
for (AstNode* itemp = nodep->stmtsp(); itemp; itemp = itemp->nextp()) {
|
|
if (itemp->name() == name) return itemp;
|
|
}
|
|
return nullptr;
|
|
}
|
|
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 fl = queueRefp->fileline();
|
|
AstClass* const processClassp
|
|
= VN_AS(getMemberp(v3Global.rootp()->stdPackagep(), "process"), Class);
|
|
AstFunc* const selfMethodp = VN_AS(getMemberp(processClassp, "self"), Func);
|
|
AstFuncRef* const processSelfp = new AstFuncRef{fl, selfMethodp, nullptr};
|
|
processSelfp->classOrPackagep(processClassp);
|
|
return new AstStmtExpr{
|
|
fl, new AstMethodCall{fl, queueRefp, "push_back", new AstArg{fl, "", processSelfp}}};
|
|
}
|
|
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();
|
|
AstClass* const processClassp
|
|
= VN_AS(getMemberp(v3Global.rootp()->stdPackagep(), "process"), Class);
|
|
// Declare queue of processes (as a global variable for simplicity)
|
|
AstVar* const processQueuep = new AstVar{
|
|
fl, VVarType::VAR, m_queueNames.get(targetp->name()), VFlagChildDType{},
|
|
new AstQueueDType{fl, VFlagChildDType{},
|
|
new AstClassRefDType{fl, processClassp, nullptr}, nullptr}};
|
|
processQueuep->lifetime(VLifetime::STATIC_EXPLICIT);
|
|
topPkgp->addStmtsp(processQueuep);
|
|
|
|
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);
|
|
}
|
|
if (beginp->stmtsp()) {
|
|
// There is no need to add it to empty block
|
|
beginp->stmtsp()->addHereThisAsNext(pushCurrentProcessp);
|
|
}
|
|
}
|
|
AstVarRef* const queueRefp = new AstVarRef{fl, topPkgp, processQueuep, VAccess::READWRITE};
|
|
AstTaskRef* const killQueueCall
|
|
= new AstTaskRef{fl, VN_AS(getMemberp(processClassp, "killQueue"), Task),
|
|
new AstArg{fl, "", queueRefp}};
|
|
killQueueCall->classOrPackagep(processClassp);
|
|
AstStmtExpr* const killStmtp = new AstStmtExpr{fl, killQueueCall};
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
static bool directlyUnderFork(const AstNode* const nodep) {
|
|
if (nodep->backp()->nextp() == nodep) return directlyUnderFork(nodep->backp());
|
|
if (VN_IS(nodep->backp(), Fork)) return true;
|
|
return false;
|
|
}
|
|
|
|
// 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(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->findSigned32DType()};
|
|
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->stmtsp());
|
|
}
|
|
void visit(AstReturn* nodep) override {
|
|
iterateChildren(nodep);
|
|
const AstFunc* const funcp = VN_CAST(m_ftaskp, Func);
|
|
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) {
|
|
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();
|
|
UASSERT_OBJ(targetp, nodep, "Unlinked disable statement");
|
|
if (VN_IS(targetp, Task)) {
|
|
nodep->v3warn(E_UNSUPPORTED, "Unsupported: disabling task by name");
|
|
} 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()) {
|
|
nodep->v3warn(E_UNSUPPORTED,
|
|
"Unsupported: disabling block that contains a fork");
|
|
} else {
|
|
// Jump to the end of the named block
|
|
AstJumpBlock* const blockp = getJumpBlock(beginp, false);
|
|
nodep->addNextHere(new AstJumpGo{nodep->fileline(), blockp});
|
|
}
|
|
} else {
|
|
if (directlyUnderFork(beginp)) {
|
|
std::vector<AstBegin*> forks{beginp};
|
|
handleDisableOnFork(nodep, forks);
|
|
} else {
|
|
nodep->v3warn(E_UNSUPPORTED, "disable isn't underneath a begin with name: '"
|
|
<< beginp->name() << "'");
|
|
}
|
|
}
|
|
} 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);
|
|
}
|