diff --git a/src/V3AstNodes.cpp b/src/V3AstNodes.cpp index 26126e5fd..8fa797adb 100644 --- a/src/V3AstNodes.cpp +++ b/src/V3AstNodes.cpp @@ -3795,7 +3795,7 @@ void AstDelay::dumpJson(std::ostream& str) const { } const char* AstDisable::broken() const { - BROKEN_RTN((m_targetp && targetRefp()) || ((!m_targetp && !targetRefp()))); + BROKEN_RTN(!m_targetp && !targetRefp()); return nullptr; } void AstDisable::dump(std::ostream& str) const { diff --git a/src/V3Fork.cpp b/src/V3Fork.cpp index 806d42079..27ff282a3 100644 --- a/src/V3Fork.cpp +++ b/src/V3Fork.cpp @@ -593,6 +593,15 @@ class ForkVisitor final : public VNVisitor { const AstConst* const constp = VN_CAST(delayp->lhsp(), Const); return constp && (constp->toUQuad() == std::numeric_limits::max()); } + static bool isDisableProcessQueueExpr(const AstNode* const nodep) { + const AstVar* varp = nullptr; + if (const AstVarRef* const refp = VN_CAST(nodep, VarRef)) { + varp = refp->varp(); + } else if (const AstMemberSel* const selp = VN_CAST(nodep, MemberSel)) { + varp = selp->varp(); + } + return varp && varp->processQueue(); + } static bool isDisableQueuePushSelfStmt(const AstNode* const nodep) { // Detect LinkJump-generated registration: // __VprocessQueue_*.push_back(std::process::self()) @@ -600,30 +609,54 @@ class ForkVisitor final : public VNVisitor { if (!stmtExprp) return false; const AstCMethodHard* const methodp = VN_CAST(stmtExprp->exprp(), CMethodHard); if (!methodp || methodp->name() != "push_back") return false; - const AstVarRef* const queueRefp = VN_CAST(methodp->fromp(), VarRef); - return queueRefp && queueRefp->varp()->processQueue(); + return isDisableProcessQueueExpr(methodp->fromp()); + } + static const AstNode* unwrapLeadingJumpBlocks(const AstNode* nodep) { + while (const AstJumpBlock* const jumpBlockp = VN_CAST(nodep, JumpBlock)) { + nodep = jumpBlockp->stmtsp(); + } + return nodep; + } + static bool isDisableQueuePushSelfPrefix(const AstNode* const nodep) { + return isDisableQueuePushSelfStmt(unwrapLeadingJumpBlocks(nodep)); + } + template + static bool insertForkSentinelAfterDisableQueuePushes(T_Owner* const ownerp, + AstNode* const firstStmtp, + AstNode* const delayp) { + AstNode* insertBeforep = firstStmtp; + while (insertBeforep && isDisableQueuePushSelfStmt(insertBeforep)) { + insertBeforep = insertBeforep->nextp(); + } + if (AstJumpBlock* const jumpBlockp = VN_CAST(insertBeforep, JumpBlock)) { + if (insertForkSentinelAfterDisableQueuePushes(jumpBlockp, jumpBlockp->stmtsp(), + delayp)) { + return true; + } + } + if (insertBeforep == firstStmtp) return false; + if (insertBeforep) { + insertBeforep->addHereThisAsNext(delayp); + } else { + ownerp->addStmtsp(delayp); + } + return true; } static void moveForkSentinelAfterDisableQueuePushes(AstBegin* const beginp) { AstNode* const firstStmtp = beginp->stmtsp(); if (!isForkJoinNoneSentinelDelay(firstStmtp)) return; - - AstNode* insertBeforep = firstStmtp->nextp(); - while (insertBeforep && isDisableQueuePushSelfStmt(insertBeforep)) { - insertBeforep = insertBeforep->nextp(); - } - if (insertBeforep == firstStmtp->nextp()) return; + AstNode* const afterSentinelp = firstStmtp->nextp(); + if (!isDisableQueuePushSelfPrefix(afterSentinelp)) return; AstNode* const delayp = firstStmtp->unlinkFrBack(); - if (insertBeforep) { - insertBeforep->addHereThisAsNext(delayp); - } else { - beginp->addStmtsp(delayp); - } + const bool moved + = insertForkSentinelAfterDisableQueuePushes(beginp, afterSentinelp, delayp); + UASSERT_OBJ(moved, beginp, "Failed to move fork sentinel after disable queue pushes"); } static bool forkIsDisableable(const AstFork* const nodep) { for (const AstBegin* itemp = nodep->forksp(); itemp; itemp = VN_AS(itemp->nextp(), Begin)) { - if (isDisableQueuePushSelfStmt(itemp->stmtsp())) return true; + if (isDisableQueuePushSelfPrefix(itemp->stmtsp())) return true; } return false; } @@ -664,13 +697,19 @@ class ForkVisitor final : public VNVisitor { } } + iterateAndNextNull(nodep->declsp()); + iterateAndNextNull(nodep->stmtsp()); + + // A plain 'join' blocks the parent until every branch finishes, so a branch cannot + // outlive the variables it references and need not be extracted into a task; just + // recurse to process any forks nested inside the branches. join_any and join_none + // branches may outlive the fork, so each branch body is wrapped in a task that + // captures the variables it references. if (nodep->joinType().join()) { - iterateChildren(nodep); + iterateAndNextNull(nodep->forksp()); return; } - iterateAndNextNull(nodep->declsp()); - iterateAndNextNull(nodep->stmtsp()); std::vector wrappedp; { VL_RESTORER(m_inFork); diff --git a/src/V3LinkDot.cpp b/src/V3LinkDot.cpp index 0bb78f8e8..e2ba8bc71 100644 --- a/src/V3LinkDot.cpp +++ b/src/V3LinkDot.cpp @@ -96,6 +96,34 @@ static string extractDottedPath(AstNode* nodep, bool& hasPartSelect) { } return ""; } +static AstNode* parentNodep(AstNode* nodep) { + while (nodep && nodep->backp() && nodep->backp()->nextp() == nodep) nodep = nodep->backp(); + return nodep ? nodep->backp() : nullptr; +} +static string lexicalDisablePath(AstNode* const targetp) { + std::vector names; + for (AstNode* curp = targetp; curp; curp = parentNodep(curp)) { + if (AstNodeBlock* const blockp = VN_CAST(curp, NodeBlock)) { + if (blockp->name() != "") names.push_back(blockp->name()); + } else if (AstNodeFTask* const ftaskp = VN_CAST(curp, NodeFTask)) { + names.push_back(ftaskp->name()); + } else if (VN_IS(curp, NodeModule)) { + break; + } + } + string path; + for (auto it = names.crbegin(); it != names.crend(); ++it) { + path = VString::dot(path, ".", *it); + } + return path; +} +static string targetInstancePath(AstNode* const targetp, const string& targetPath) { + const string lexicalPath = lexicalDisablePath(targetp); + if (lexicalPath == "" || targetPath == lexicalPath) return ""; + const string suffix = "." + lexicalPath; + if (!VString::endsWith(targetPath, suffix)) return ""; + return targetPath.substr(0, targetPath.size() - suffix.size()); +} // ###################################################################### // Matcher classes (for suggestion matching) @@ -6163,6 +6191,9 @@ class LinkDotResolveVisitor final : public VNVisitor { void visit(AstDisable* nodep) override { LINKDOT_VISIT_START(); checkNoDot(nodep); + bool hasPartSelect = false; + const string targetPath + = nodep->targetRefp() ? extractDottedPath(nodep->targetRefp(), hasPartSelect) : ""; VL_RESTORER_COPY(m_ds); m_ds.init(m_curSymp); m_ds.m_dotPos = DP_FIRST; @@ -6179,8 +6210,17 @@ class LinkDotResolveVisitor final : public VNVisitor { pushDeletep(nodep->unlinkFrBack()); } if (nodep->targetp()) { - // If the target is already linked, there is no need to store reference as child - VL_DO_DANGLING(nodep->targetRefp()->unlinkFrBack()->deleteTree(), nodep); + AstNodeExpr* oldRefp = nodep->targetRefp()->unlinkFrBack(); + VL_DO_DANGLING(oldRefp->deleteTree(), oldRefp); + if (!hasPartSelect) { + const string instancePath = targetInstancePath(nodep->targetp(), targetPath); + if (instancePath != "") { + // Keep only the instance prefix so V3LinkJump can reference the selected + // instance's process queue without reparsing the disable target. + nodep->targetRefp( + new AstVarXRef{nodep->fileline(), "", instancePath, VAccess::READ}); + } + } } } } diff --git a/src/V3LinkJump.cpp b/src/V3LinkJump.cpp index 70115033f..ccca50d95 100644 --- a/src/V3LinkJump.cpp +++ b/src/V3LinkJump.cpp @@ -68,6 +68,7 @@ class LinkJumpVisitor final : public VNVisitor { "__VprocessQueue"}; // Names for queues needed for 'disable' handling std::unordered_map m_taskDisableQueues; // Per-task process queues std::unordered_map m_beginDisableQueues; // Per-begin process queues + std::unordered_map m_forkDisableQueues; // Per-fork process queues std::unordered_map m_taskDisableBegins; // Per-task process wrappers std::unordered_map @@ -167,6 +168,38 @@ class LinkJumpVisitor final : public VNVisitor { } return false; } + static AstNode* parentNodep(AstNode* nodep) { + while (nodep && nodep->backp() && nodep->backp()->nextp() == nodep) nodep = nodep->backp(); + return nodep ? nodep->backp() : nullptr; + } + static AstNodeModule* findOwnerModulep(AstNode* nodep) { + for (AstNode* curp = nodep; curp; curp = parentNodep(curp)) { + if (AstNodeModule* const modp = VN_CAST(curp, NodeModule)) return modp; + } + return nullptr; + } + static std::string targetInstancePath(const AstDisable* const nodep) { + if (const AstVarXRef* const xrefp = VN_CAST(nodep->targetRefp(), VarXRef)) { + return xrefp->dotted(); + } + return ""; + } + static std::string queueDotted(const AstDisable* const nodep, + const AstVar* const processQueuep) { + return processQueuep->isTemp() ? targetInstancePath(nodep) : ""; + } + AstBegin* innerForkBranchp(const AstNodeBlock* const targetp) const { + AstBegin* innerForkBranchp = nullptr; + AstNodeBlock* prevBlockp = nullptr; + for (AstNodeBlock* const blockp : vlstd::reverse_view(m_blockStack)) { + if (!innerForkBranchp && VN_IS(blockp, Fork)) { + innerForkBranchp = VN_CAST(prevBlockp, Begin); + } + if (blockp == targetp) return innerForkBranchp; + prevBlockp = blockp; + } + return nullptr; + } static AstStmtExpr* getQueuePushProcessSelfp(AstVarRef* const queueRefp) { // Constructs queue.push_back(std::process::self()) statement FileLine* const flp = queueRefp->fileline(); @@ -175,15 +208,24 @@ class LinkJumpVisitor final : public VNVisitor { new AstMethodCall{flp, queueRefp, "push_back", new AstArg{flp, "", v3Global.rootp()->stdPackageProcessSelfp(flp)}}}; } - static AstStmtExpr* getQueuePushProcessSelfp(FileLine* const fl, AstVar* const processQueuep) { + static AstNodeVarRef* newQueueRefp(FileLine* const fl, AstVar* const processQueuep, + const VAccess& access, const std::string& dotted = "") { + if (dotted != "") return new AstVarXRef{fl, processQueuep, dotted, access}; + if (!processQueuep->lifetime().isStatic() || processQueuep->isTemp()) { + return new AstVarRef{fl, processQueuep, access}; + } AstPackage* const topPkgp = v3Global.rootp()->dollarUnitPkgAddp(); + return new AstVarRef{fl, topPkgp, processQueuep, access}; + } + static AstStmtExpr* getQueuePushProcessSelfp(FileLine* const fl, AstVar* const processQueuep) { AstVarRef* const queueWriteRefp - = new AstVarRef{fl, topPkgp, processQueuep, VAccess::WRITE}; + = VN_AS(newQueueRefp(fl, processQueuep, VAccess::WRITE), VarRef); 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}; + static AstStmtExpr* getQueueKillStmtp(FileLine* const fl, AstVar* const processQueuep, + const std::string& dotted = "") { + AstNodeVarRef* const queueRefp + = newQueueRefp(fl, processQueuep, VAccess::READWRITE, dotted); AstTaskRef* killQueueCall = nullptr; for (AstNode* itemp = v3Global.rootp()->stdPackageProcessp()->stmtsp(); itemp; itemp = itemp->nextp()) { @@ -197,24 +239,14 @@ class LinkJumpVisitor final : public VNVisitor { killQueueCall->classOrPackagep(v3Global.rootp()->stdPackageProcessp()); return new AstStmtExpr{fl, killQueueCall}; } - static void prependStmtsp(AstNodeFTask* const nodep, AstNode* const stmtp) { + template + static void prependStmtsp(T_Node* 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; @@ -232,18 +264,33 @@ class LinkJumpVisitor final : public VNVisitor { m_taskDisableBegins.emplace(taskp, taskBodyp); return taskBodyp; } - AstVar* getProcessQueuep(AstNode* const nodep, FileLine* const fl) { - AstPackage* const topPkgp = v3Global.rootp()->dollarUnitPkgAddp(); + AstVar* newProcessQueuep(AstNode* const nodep, FileLine* const fl, const VVarType& varType) { AstVar* const processQueuep = new AstVar{ - fl, VVarType::VAR, m_queueNames.get(nodep->name()), VFlagChildDType{}, + fl, varType, 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* getProcessQueuep(AstNode* const nodep, FileLine* const fl) { + AstNodeModule* ownerp = findOwnerModulep(nodep); + if (!ownerp) ownerp = m_modp; + UASSERT_OBJ(ownerp, nodep, "Disable queue owner is not under a module"); + + if (VN_IS(ownerp, Package) || VN_IS(ownerp, Class)) { + AstPackage* const topPkgp = v3Global.rootp()->dollarUnitPkgAddp(); + AstVar* const processQueuep = newProcessQueuep(nodep, fl, VVarType::VAR); + processQueuep->lifetime(VLifetime::STATIC_EXPLICIT); + topPkgp->addStmtsp(processQueuep); + return processQueuep; + } + + AstVar* const processQueuep = newProcessQueuep(nodep, fl, VVarType::MODULETEMP); + processQueuep->lifetime(VLifetime::STATIC_EXPLICIT); + ownerp->addStmtsp(processQueuep); return processQueuep; } AstVar* getOrCreateTaskDisableQueuep(AstTask* const taskp, FileLine* const fl) { @@ -274,6 +321,20 @@ class LinkJumpVisitor final : public VNVisitor { m_beginDisableBegins.emplace(beginp, beginBodyp); return beginBodyp; } + void prependForkBranchQueuePushes(AstFork* const forkp, AstVar* const processQueuep, + FileLine* const fl, bool needProcess) { + for (AstBegin* branchp = forkp->forksp(); branchp; + branchp = VN_AS(branchp->nextp(), Begin)) { + if (needProcess) branchp->setNeedProcess(); + prependStmtsp(branchp, getQueuePushProcessSelfp(fl, processQueuep)); + } + } + void prependNestedForkBranchQueuePushes(AstNode* const nodep, AstVar* const processQueuep, + FileLine* const fl, bool needProcess) { + nodep->foreach([&](AstFork* const forkp) { + prependForkBranchQueuePushes(forkp, processQueuep, fl, needProcess); + }); + } AstVar* getOrCreateBeginDisableQueuep(AstBegin* const beginp, FileLine* const fl) { const auto it = m_beginDisableQueues.find(beginp); if (it != m_beginDisableQueues.end()) return it->second; @@ -285,24 +346,43 @@ class LinkJumpVisitor final : public VNVisitor { // 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); - } - }); + prependNestedForkBranchQueuePushes(beginBodyp, processQueuep, fl, false); m_beginDisableQueues.emplace(beginp, processQueuep); return processQueuep; } - void handleDisableOnFork(AstDisable* const nodep, const std::vector& 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. + AstVar* getOrCreateForkDisableQueuep(AstFork* const forkp, FileLine* const fl) { + const auto it = m_forkDisableQueues.find(forkp); + if (it != m_forkDisableQueues.end()) return it->second; + + AstVar* const processQueuep = getProcessQueuep(forkp, fl); + prependForkBranchQueuePushes(forkp, processQueuep, fl, true); + // Disabling a fork must also terminate detached descendants created by nested forks + // under each branch, so track nested fork branch processes in the same queue. + for (AstBegin* branchp = forkp->forksp(); branchp; + branchp = VN_AS(branchp->nextp(), Begin)) { + prependNestedForkBranchQueuePushes(branchp, processQueuep, fl, true); + } + m_forkDisableQueues.emplace(forkp, processQueuep); + return processQueuep; + } + AstStmtExpr* insertKillStmtp(AstDisable* const nodep, AstVar* const processQueuep) { FileLine* const fl = nodep->fileline(); - AstNode* const targetp = nodep->targetp(); + AstStmtExpr* const killStmtp + = getQueueKillStmtp(fl, processQueuep, queueDotted(nodep, processQueuep)); + nodep->addNextHere(killStmtp); + return killStmtp; + } + void addJumpAfterKill(AstStmtExpr* const killStmtp, AstNode* const targetp) { + AstJumpBlock* const jmpBlockp = getJumpBlock(targetp, false); + killStmtp->addNextHere(new AstJumpGo{killStmtp->fileline(), jmpBlockp}); + } + void handleDisableOnFork(AstDisable* const nodep) { + // The support utilizes the process::kill()` method. For each disabled fork a queue of + // processes is declared. At the beginning of each fork branch (and each nested fork + // branch) its process handle is pushed to the queue. The `disable` statement is replaced + // with calling `kill()` on each element of the queue. + FileLine* const fl = nodep->fileline(); + AstFork* const targetp = VN_AS(nodep->targetp(), Fork); if (m_ftaskp) { if (!m_ftaskp->exists( [targetp](const AstNodeBlock* blockp) -> bool { return blockp == targetp; })) { @@ -311,35 +391,18 @@ class LinkJumpVisitor final : public VNVisitor { } } - 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); + AstVar* const processQueuep = getOrCreateForkDisableQueuep(targetp, fl); + AstStmtExpr* const killStmtp = insertKillStmtp(nodep, processQueuep); - 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; - } + // 'process::kill' does not immediately kill the current process executing the disable + // statement (because it's in the running state). If the disable runs under the target + // fork, jump to the end of the innermost enclosing fork branch (the branch that holds the + // running process) so statements after the disable do not execute. Targeting the innermost + // branch keeps the jump inside a single emitted coroutine even when the disable sits in a + // nested sub-fork; targeting the outer target fork's branch would cross a coroutine + // boundary once forks are split into separate functions. + if (AstBegin* const branchp = innerForkBranchp(targetp)) { + addJumpAfterKill(killStmtp, branchp); } } // VISITORS @@ -522,8 +585,7 @@ class LinkJumpVisitor final : public VNVisitor { } 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); + AstStmtExpr* const killStmtp = insertKillStmtp(nodep, processQueuep); // 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. @@ -531,15 +593,10 @@ class LinkJumpVisitor final : public VNVisitor { 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}); + addJumpAfterKill(killStmtp, jumpTargetp); } - } else if (AstFork* const forkp = VN_CAST(targetp, Fork)) { - std::vector forks; - for (AstBegin* itemp = forkp->forksp(); itemp; itemp = VN_AS(itemp->nextp(), Begin)) { - forks.push_back(itemp); - } - handleDisableOnFork(nodep, forks); + } else if (VN_IS(targetp, Fork)) { + handleDisableOnFork(nodep); } else if (AstBegin* const beginp = VN_CAST(targetp, Begin)) { if (existsBlockAbove(beginp->name())) { if (!beginp->user3()) { @@ -549,30 +606,20 @@ class LinkJumpVisitor final : public VNVisitor { } else { AstVar* const processQueuep = getOrCreateBeginDisableQueuep(beginp, nodep->fileline()); - AstStmtExpr* const killStmtp - = getQueueKillStmtp(nodep->fileline(), processQueuep); - nodep->addNextHere(killStmtp); + AstStmtExpr* const killStmtp = insertKillStmtp(nodep, processQueuep); // 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}); - } + AstBegin* const branchp = innerForkBranchp(beginp); + AstBegin* const jumpTargetp = branchp ? branchp : m_beginDisableBegins[beginp]; + UASSERT_OBJ(jumpTargetp, nodep, "Missing disable jump target"); + addJumpAfterKill(killStmtp, jumpTargetp); } } else { AstVar* const processQueuep = getOrCreateBeginDisableQueuep(beginp, nodep->fileline()); - AstStmtExpr* const killStmtp = getQueueKillStmtp(nodep->fileline(), processQueuep); - nodep->addNextHere(killStmtp); + insertKillStmtp(nodep, processQueuep); } } else { nodep->v3fatalSrc("Disable linked with node of unhandled type " diff --git a/src/V3Timing.cpp b/src/V3Timing.cpp index 0c2e104e9..cd69e29af 100644 --- a/src/V3Timing.cpp +++ b/src/V3Timing.cpp @@ -749,15 +749,32 @@ class TimingControlVisitor final : public VNVisitor { addDebugInfo(donep); beginp->addStmtsp(donep->makeStmt()); } + static bool isDisableProcessQueueExpr(const AstNode* const nodep) { + const AstVar* varp = nullptr; + if (const AstVarRef* const refp = VN_CAST(nodep, VarRef)) { + varp = refp->varp(); + } else if (const AstMemberSel* const selp = VN_CAST(nodep, MemberSel)) { + varp = selp->varp(); + } + return varp && varp->processQueue(); + } static bool hasDisableQueuePushSelfPrefix(const AstBegin* const beginp) { // LinkJump prepends disable-by-name registration as: // __VprocessQueue_*.push_back(std::process::self()) - const AstStmtExpr* const stmtExprp = VN_CAST(beginp->stmtsp(), StmtExpr); - if (!stmtExprp) return false; - const AstCMethodHard* const methodp = VN_CAST(stmtExprp->exprp(), CMethodHard); - if (!methodp || methodp->name() != "push_back") return false; - const AstVarRef* const queueRefp = VN_CAST(methodp->fromp(), VarRef); - return queueRefp && queueRefp->varp()->processQueue(); + // By this pass V3LiftExpr has lifted the std::process::self() argument into a preceding + // temporary (and left a leading comment), so the push_back is no longer necessarily the + // first statement. Scan across the leading registration statements for it, stopping at the + // fork-start sentinel or the branch body. The branch's own registration is at the front; + // registrations for nested forks live in sub-blocks and so are not in this statement list. + for (const AstNode* stmtp = beginp->stmtsp(); stmtp; stmtp = stmtp->nextp()) { + if (VN_IS(stmtp, Comment)) continue; + const AstStmtExpr* const stmtExprp = VN_CAST(stmtp, StmtExpr); + if (!stmtExprp) break; + const AstCMethodHard* const methodp = VN_CAST(stmtExprp->exprp(), CMethodHard); + if (!methodp || methodp->name() != "push_back") continue; + if (isDisableProcessQueueExpr(methodp->fromp())) return true; + } + return false; } // Register a callback so killing a process-backed fork branch decrements the join counter void addForkOnKill(AstBegin* const beginp, AstVarScope* const forkVscp) const { diff --git a/test_regress/t/t_disable_begin_instances.py b/test_regress/t/t_disable_begin_instances.py new file mode 100755 index 000000000..fdbd25744 --- /dev/null +++ b/test_regress/t/t_disable_begin_instances.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# 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: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=["--binary", "-Wno-ZERODLY"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_disable_begin_instances.v b/test_regress/t/t_disable_begin_instances.v new file mode 100644 index 000000000..a568b8ea1 --- /dev/null +++ b/test_regress/t/t_disable_begin_instances.v @@ -0,0 +1,67 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// Disabling a named begin block in one module instance must not terminate +// branches of the same syntactic block in another module instance. + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +// verilog_format: on + +module child ( + input bit do_disable, + output bit survived, + output bit done +); + + initial begin : blk + fork + begin + #5; + survived = 1'b1; + end + join_none + if (do_disable) begin + #1; + disable blk; + end + else begin + #6; + end + done = 1'b1; + end +endmodule + +module t; + + bit survived0; + bit done0; + bit survived1; + bit done1; + + child child0 ( + .do_disable(1'b1), + .survived(survived0), + .done(done0) + ); + + child child1 ( + .do_disable(1'b0), + .survived(survived1), + .done(done1) + ); + + initial begin + #8; + `checkd(done0, 1'b0); + `checkd(done1, 1'b1); + `checkd(survived0, 1'b0); + `checkd(survived1, 1'b1); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_disable_begin_self_fork.py b/test_regress/t/t_disable_begin_self_fork.py new file mode 100755 index 000000000..6fe7d000c --- /dev/null +++ b/test_regress/t/t_disable_begin_self_fork.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# 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: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=["--binary"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_disable_begin_self_fork.v b/test_regress/t/t_disable_begin_self_fork.v new file mode 100644 index 000000000..a674ed23b --- /dev/null +++ b/test_regress/t/t_disable_begin_self_fork.v @@ -0,0 +1,41 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// A named begin block that contains a fork still has to behave like a normal +// named block when it disables itself: execution resumes after the block, so +// statements after the disable inside the block must not run. All activity +// enabled within the block, including forked child processes, must terminate. + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +// verilog_format: on + +module t; + + bit after_disable = 1'b0; + bit fork_survived = 1'b0; + + initial begin : blk + fork + begin + #5; + fork_survived = 1'b1; + end + join_none + #1; + disable blk; + after_disable = 1'b1; + end + + initial begin + #10; + `checkd(after_disable, 1'b0); + `checkd(fork_survived, 1'b0); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_disable_fork_deep.py b/test_regress/t/t_disable_fork_deep.py new file mode 100755 index 000000000..fdbd25744 --- /dev/null +++ b/test_regress/t/t_disable_fork_deep.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# 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: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=["--binary", "-Wno-ZERODLY"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_disable_fork_deep.v b/test_regress/t/t_disable_fork_deep.v new file mode 100644 index 000000000..8892d702f --- /dev/null +++ b/test_regress/t/t_disable_fork_deep.v @@ -0,0 +1,205 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// Disabling a named fork from a deeply nested sub-fork branch must terminate +// every process spawned within that fork, at all nesting levels, including +// branches suspended on a delay. Previously this crashed Verilator: the bail-out +// jump emitted for the disable targeted the outer fork branch and crossed a +// coroutine boundary once the nested forks were split into separate functions. +// Even disregarding the crash, processes of nested sub-forks were not registered +// for the kill, so a nested sibling survived and its $stop fired. +// +// The disable must also let the process that owns the disabled fork's 'join' +// resume after the join (IEEE 1800-2023 9.6.2): once the block is terminated, +// execution continues at the point after it. The *_resumed bits below latch that +// the parent ran past its join; a hang there leaves the bit clear and is caught +// at the end. A fork branch that itself holds a nested fork is killed while +// suspended at the inner join, so it must register a kill hook on its parent +// fork or the parent join counter is never decremented and the parent hangs. + +// verilog_format: off +`define stop $stop +`define checkt(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0t exp=%0t (%t !== %t)\n", `__FILE__,`__LINE__, (gotv), (expv), `"gotv`", `"expv`"); `stop; end while(0); +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +// verilog_format: on + +module t; + + bit d2_resumed = 1'b0; + bit d3_resumed = 1'b0; + bit d2a_resumed = 1'b0; + bit d2r_resumed = 1'b0; + bit ja1_resumed = 1'b0; + + // fork..join nested two levels: disabling the outer named fork from inside the + // inner fork must kill the inner delayed sibling and the outer delayed sibling. + initial begin + fork : fork_d2 + begin + fork + begin + `checkt($time, 0); + disable fork_d2; + $stop; + end + begin + `checkt($time, 0); + #1 $stop; + end + join + end + begin + `checkt($time, 0); + #1 $stop; + end + join + `checkt($time, 0); + d2_resumed = 1'b1; + end + + // fork..join nested three levels: disabling the outermost named fork from the + // innermost branch must kill the delayed siblings at every level. + initial begin + fork : fork_d3 + begin + fork + begin + fork + begin + `checkt($time, 0); + disable fork_d3; + $stop; + end + begin + `checkt($time, 0); + #1 $stop; + end + join + end + begin + `checkt($time, 0); + #1 $stop; + end + join + end + begin + `checkt($time, 0); + #1 $stop; + end + join + `checkt($time, 0); + d3_resumed = 1'b1; + end + + // fork..join_any nested two levels. + initial begin + fork : fork_d2a + begin + fork + begin + `checkt($time, 0); + disable fork_d2a; + $stop; + end + begin + `checkt($time, 0); + #1 $stop; + end + join_any + end + begin + `checkt($time, 0); + #1 $stop; + end + join_any + `checkt($time, 0); + d2a_resumed = 1'b1; + end + + // fork..join_none nested two levels. + initial begin + fork : fork_d2n + begin + fork + begin + `checkt($time, 0); + disable fork_d2n; + $stop; + end + begin + `checkt($time, 0); + #1 $stop; + end + join_none + end + begin + `checkt($time, 0); + #1 $stop; + end + join_none + end + + // fork..join nested two levels, but the disabling branch comes after the + // delayed sibling (in source order) at both levels. + initial begin + fork : fork_d2r + begin + `checkt($time, 0); + #1 $stop; + end + begin + fork + begin + `checkt($time, 0); + #1 $stop; + end + begin + `checkt($time, 0); + disable fork_d2r; + $stop; + end + join + end + join + `checkt($time, 0); + d2r_resumed = 1'b1; + end + + // fork..join_any whose only branch holds the nested fork that gets disabled. + // join_any needs one branch to complete, but the only branch is killed while + // suspended at its inner join, so the parent resumes only if that branch + // decrements the join counter on kill. + initial begin + fork : fork_ja1 + begin + fork + begin + `checkt($time, 0); + disable fork_ja1; + $stop; + end + begin + `checkt($time, 0); + #1 $stop; + end + join + end + join_any + `checkt($time, 0); + ja1_resumed = 1'b1; + end + + initial begin + #10; + `checkd(d2_resumed, 1'b1); + `checkd(d3_resumed, 1'b1); + `checkd(d2a_resumed, 1'b1); + `checkd(d2r_resumed, 1'b1); + `checkd(ja1_resumed, 1'b1); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_disable_fork_hier_inst.py b/test_regress/t/t_disable_fork_hier_inst.py new file mode 100755 index 000000000..fdbd25744 --- /dev/null +++ b/test_regress/t/t_disable_fork_hier_inst.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# 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: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=["--binary", "-Wno-ZERODLY"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_disable_fork_hier_inst.v b/test_regress/t/t_disable_fork_hier_inst.v new file mode 100644 index 000000000..0488e65a8 --- /dev/null +++ b/test_regress/t/t_disable_fork_hier_inst.v @@ -0,0 +1,60 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// A hierarchical disable of a named fork in one module instance must terminate +// only that instance's fork branch. IEEE 1800-2023 A.6.5 permits disable of a +// hierarchical_block_identifier. + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +// verilog_format: on + +module child ( + output bit survived, + output bit done +); + + initial begin : init + fork : fork_blk + begin + #5; + survived = 1'b1; + end + join_none + #6; + done = 1'b1; + end +endmodule +module t; + + bit survived0; + bit done0; + bit survived1; + bit done1; + + child child0 ( + .survived(survived0), + .done(done0) + ); + + child child1 ( + .survived(survived1), + .done(done1) + ); + + initial begin + #1; + disable child0.init.fork_blk; + #7; + `checkd(done0, 1'b1); + `checkd(done1, 1'b1); + `checkd(survived0, 1'b0); + `checkd(survived1, 1'b1); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_disable_fork_instances.py b/test_regress/t/t_disable_fork_instances.py new file mode 100755 index 000000000..fdbd25744 --- /dev/null +++ b/test_regress/t/t_disable_fork_instances.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# 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: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=["--binary", "-Wno-ZERODLY"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_disable_fork_instances.v b/test_regress/t/t_disable_fork_instances.v new file mode 100644 index 000000000..4da59f0ef --- /dev/null +++ b/test_regress/t/t_disable_fork_instances.v @@ -0,0 +1,81 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// Disabling a named fork in one module instance must not terminate branches of +// the same syntactic fork in another module instance. + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +// verilog_format: on + +module child( + input bit do_disable, + output bit survived_a, + output bit survived_b, + output bit done +); + + initial begin + fork : fork_blk + begin + if (do_disable) begin + #1; + disable fork_blk; + $stop; + end else begin + #6; + survived_a = 1'b1; + end + end + begin + if (do_disable) begin + #5 $stop; + end else begin + #6; + survived_b = 1'b1; + end + end + join + done = 1'b1; + end +endmodule + +module t; + + bit do_disable0 = 1'b1; + bit do_disable1 = 1'b0; + bit survived_a0; + bit survived_b0; + bit done0; + bit survived_a1; + bit survived_b1; + bit done1; + + child child0( + .do_disable(do_disable0), + .survived_a(survived_a0), + .survived_b(survived_b0), + .done(done0) + ); + + child child1( + .do_disable(do_disable1), + .survived_a(survived_a1), + .survived_b(survived_b1), + .done(done1) + ); + + initial begin + #7; + `checkd(done0, 1'b1); + `checkd(done1, 1'b1); + `checkd(survived_a1, 1'b1); + `checkd(survived_b1, 1'b1); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_disable_fork_join.py b/test_regress/t/t_disable_fork_join.py old mode 100644 new mode 100755 diff --git a/test_regress/t/t_disable_fork_join.v b/test_regress/t/t_disable_fork_join.v index 1db7277a4..8cf504faa 100644 --- a/test_regress/t/t_disable_fork_join.v +++ b/test_regress/t/t_disable_fork_join.v @@ -49,6 +49,25 @@ module t; join end + // fork..join: the delayed sibling also contains an unreachable disable, which + // makes LinkJump wrap its registration in a JumpBlock. It must still register + // before the synthetic fork-start delay so the earlier disable kills it. + initial begin + fork : fork_blk_wrap + begin + `checkt($time, 0); + disable fork_blk_wrap; + $stop; + end + begin + `checkt($time, 0); + #1; + if ($c("false")) disable fork_blk_wrap; + $stop; + end + join + end + // fork..join_any: the disabling branch comes first, the delayed sibling second. initial begin fork : fork_blk2a @@ -79,6 +98,24 @@ module t; join_any end + // fork..join_any: the delayed sibling's own unreachable disable again forces a + // JumpBlock wrapper around its registration. + initial begin + fork : fork_blk_wrap_any + begin + `checkt($time, 0); + disable fork_blk_wrap_any; + $stop; + end + begin + `checkt($time, 0); + #1; + if ($c("false")) disable fork_blk_wrap_any; + $stop; + end + join_any + end + // fork..join that can be disabled, but takes the path where disable is not // reached: both branches must run to completion with their timing intact. initial begin diff --git a/test_regress/t/t_disable_fork_reentrant.py b/test_regress/t/t_disable_fork_reentrant.py new file mode 100755 index 000000000..fdbd25744 --- /dev/null +++ b/test_regress/t/t_disable_fork_reentrant.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# 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: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=["--binary", "-Wno-ZERODLY"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_disable_fork_reentrant.v b/test_regress/t/t_disable_fork_reentrant.v new file mode 100644 index 000000000..c65b18c46 --- /dev/null +++ b/test_regress/t/t_disable_fork_reentrant.v @@ -0,0 +1,137 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// A disable of a named fork inside one dynamic task invocation must kill +// branches of the same syntactic fork in every concurrent invocation of that +// task. IEEE 1800-2023 9.6.2 says disabling an automatic task or a block inside +// an automatic task proceeds as for regular tasks for all concurrent executions +// of the task. +// +// The cross-module case checks the complementary instance scoping rule: the same +// task declaration can be instantiated under different module instances in +// different wrapper modules, and disabling the named fork in one instance must +// not terminate the other instance's fork. + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +// verilog_format: on + +module child_task_mod ( + input bit do_disable, + output bit survived, + output bit task_done +); + + task automatic run(); + fork : fork_blk + begin + if (do_disable) begin + #1; + disable fork_blk; + $stop; + end + else begin + #6; + end + end + begin + #5; + survived = 1'b1; + end + join + task_done = 1'b1; + endtask + + initial run(); +endmodule + +module disable_wrapper ( + output bit survived, + output bit task_done +); + child_task_mod child ( + .do_disable(1'b1), + .survived(survived), + .task_done(task_done) + ); +endmodule + +module keep_wrapper ( + output bit survived, + output bit task_done +); + child_task_mod child ( + .do_disable(1'b0), + .survived(survived), + .task_done(task_done) + ); +endmodule + +module t; + + bit [1:0] survived_a = 2'b00; + bit [1:0] survived_b = 2'b00; + bit [1:0] task_done = 2'b00; + bit wrapper_disable_survived; + bit wrapper_disable_done; + bit wrapper_keep_survived; + bit wrapper_keep_done; + + disable_wrapper wrapper0 ( + .survived(wrapper_disable_survived), + .task_done(wrapper_disable_done) + ); + + keep_wrapper wrapper1 ( + .survived(wrapper_keep_survived), + .task_done(wrapper_keep_done) + ); + + task automatic run(input int id, input bit do_disable); + fork : fork_blk + begin + if (do_disable) begin + #1; + disable fork_blk; + $stop; + end + else begin + #6; + survived_a[id] = 1'b1; + end + end + begin + if (do_disable) begin + #5 $stop; + end + else begin + #6; + survived_b[id] = 1'b1; + end + end + join + task_done[id] = 1'b1; + endtask + + initial begin + fork + run(0, 1'b1); + run(1, 1'b0); + join + #1; + `checkd(task_done, 2'b11); + `checkd(survived_a, 2'b00); + `checkd(survived_b, 2'b00); + #6; + `checkd(wrapper_disable_done, 1'b1); + `checkd(wrapper_keep_done, 1'b1); + `checkd(wrapper_disable_survived, 1'b0); + `checkd(wrapper_keep_survived, 1'b1); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_disable_task_instances.py b/test_regress/t/t_disable_task_instances.py new file mode 100755 index 000000000..6fe7d000c --- /dev/null +++ b/test_regress/t/t_disable_task_instances.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# 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: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=["--binary"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_disable_task_instances.v b/test_regress/t/t_disable_task_instances.v new file mode 100644 index 000000000..28911ccb6 --- /dev/null +++ b/test_regress/t/t_disable_task_instances.v @@ -0,0 +1,69 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// Disabling a module task by its local name in one module instance must not +// terminate the same task declaration's activation in another module instance. +// Each module instance and task definition creates a distinct hierarchical scope. + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +// verilog_format: on + +module child ( + input bit do_disable, + output bit survived, + output bit done +); + + task automatic run(); + #5; + survived = 1'b1; + endtask + + initial begin + fork + run(); + join_none + if (do_disable) begin + #1; + disable run; + end + else begin + #6; + end + done = 1'b1; + end +endmodule +module t; + + bit survived0; + bit done0; + bit survived1; + bit done1; + + child child0 ( + .do_disable(1'b1), + .survived(survived0), + .done(done0) + ); + + child child1 ( + .do_disable(1'b0), + .survived(survived1), + .done(done1) + ); + + initial begin + #8; + `checkd(done0, 1'b1); + `checkd(done1, 1'b1); + `checkd(survived0, 1'b0); + `checkd(survived1, 1'b1); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule