This commit is contained in:
Marco Bartoli 2026-07-11 19:13:13 -07:00 committed by GitHub
commit f0b3b56fe0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1229 additions and 129 deletions

View File

@ -730,6 +730,7 @@ public:
// ACCESSORS for specific types
// Alas these can't be virtual or they break when passed a nullptr
bool isDisableQueuePushSelfStmt();
inline bool isClassHandleValue() const;
inline bool isNull() const;
inline bool isZero() const;

View File

@ -108,6 +108,21 @@ int AstNodeSel::bitConst() const {
return (constp ? constp->toSInt() : 0);
}
bool AstNode::isDisableQueuePushSelfStmt() {
// Detect LinkJump-generated registration:
// __VprocessQueue_*.push_back(std::process::self())
AstStmtExpr* const stmtExprp = VN_CAST(this, StmtExpr);
if (!stmtExprp) return false;
AstCMethodHard* const methodp = VN_CAST(stmtExprp->exprp(), CMethodHard);
if (!methodp || methodp->name() != "push_back") return false;
AstNode* const basep = AstArraySel::baseFromp(methodp->fromp(), false);
if (AstVarRef* const refp = VN_CAST(basep, VarRef)) return refp->varp()->processQueue();
if (AstMemberSel* const selp = VN_CAST(basep, MemberSel)) {
return selp->varp() && selp->varp()->processQueue();
}
return false;
}
void AstNodeStmt::dump(std::ostream& str) const { this->AstNode::dump(str); }
void AstNodeStmt::dumpJson(std::ostream& str) const { dumpJsonGen(str); }
@ -3795,7 +3810,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 {

View File

@ -593,32 +593,50 @@ class ForkVisitor final : public VNVisitor {
const AstConst* const constp = VN_CAST(delayp->lhsp(), Const);
return constp && (constp->toUQuad() == std::numeric_limits<uint64_t>::max());
}
static bool isDisableQueuePushSelfStmt(const AstNode* const nodep) {
// Detect LinkJump-generated registration:
// __VprocessQueue_*.push_back(std::process::self())
const AstStmtExpr* const stmtExprp = VN_CAST(nodep, 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();
static bool isDisableQueuePushSelfPrefix(AstNode* nodep) {
while (AstJumpBlock* const jumpBlockp = VN_CAST(nodep, JumpBlock)) {
nodep = jumpBlockp->stmtsp();
}
return nodep && nodep->isDisableQueuePushSelfStmt();
}
template <typename T_Owner>
static bool insertForkSentinelAfterDisableQueuePushes(T_Owner* const ownerp,
AstNode* const firstStmtp,
AstNode* const delayp) {
AstNode* insertBeforep = firstStmtp;
while (insertBeforep && insertBeforep->isDisableQueuePushSelfStmt()) {
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(AstFork* const nodep) {
for (AstBegin* itemp = nodep->forksp(); itemp; itemp = VN_AS(itemp->nextp(), Begin)) {
if (isDisableQueuePushSelfPrefix(itemp->stmtsp())) return true;
}
return false;
}
// VISITORS
@ -635,17 +653,14 @@ class ForkVisitor final : public VNVisitor {
}
void visit(AstFork* nodep) override {
if (nodep->joinType().join()) {
iterateChildren(nodep);
return;
}
// IEEE 1800-2023 9.3.2: In all cases, processes spawned by a fork-join block shall not
// start executing until the parent process is blocked or terminates.
// Because join and join_any block the parent process, it is only needed when join_none
// is used.
if (nodep->joinType().joinNone()) {
UINFO(9, "Visiting fork..join_none " << nodep);
// start executing until the parent process is blocked or terminates. Because join and
// join_any block the parent process, deferring branch start with a synthetic #0 delay is
// normally only needed for join_none. A fork that can be disabled by name needs the same
// deferral for every join type so all branches register their processes before any branch
// body can disable the block.
if (nodep->joinType().joinNone() || forkIsDisableable(nodep)) {
UINFO(9, "Adding fork branch start sentinels " << nodep);
FileLine* fl = nodep->fileline();
// We use a sentinel value of UINT64_MAX to mark this delay so that it goes to the
// ACTIVE region with a delay value of 0.
@ -663,6 +678,17 @@ 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()) {
iterateAndNextNull(nodep->forksp());
return;
}
std::vector<AstBegin*> wrappedp;
{
VL_RESTORER(m_inFork);

View File

@ -96,6 +96,30 @@ static string extractDottedPath(AstNode* nodep, bool& hasPartSelect) {
}
return "";
}
static string lexicalDisablePath(AstNode* const targetp) {
std::vector<string> names;
for (AstNode* curp = targetp; curp; curp = curp->aboveLoopp()) {
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 +6187,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 +6206,16 @@ 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);
nodep->targetRefp()->unlinkFrBack()->deleteTree();
if (!hasPartSelect) {
const string instancePath = targetInstancePath(nodep->targetp(), targetPath);
if (!instancePath.empty()) {
// 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});
}
}
}
}
}

View File

@ -68,6 +68,7 @@ class LinkJumpVisitor final : public VNVisitor {
"__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 AstFork*, AstVar*> m_forkDisableQueues; // Per-fork process queues
std::unordered_map<const AstTask*, AstBegin*>
m_taskDisableBegins; // Per-task process wrappers
std::unordered_map<const AstBegin*, AstBegin*>
@ -167,6 +168,34 @@ class LinkJumpVisitor final : public VNVisitor {
}
return false;
}
static AstNodeModule* findOwnerModulep(AstNode* nodep) {
for (AstNode* curp = nodep; curp; curp = curp->aboveLoopp()) {
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 +204,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 +235,13 @@ class LinkJumpVisitor final : public VNVisitor {
killQueueCall->classOrPackagep(v3Global.rootp()->stdPackageProcessp());
return new AstStmtExpr{fl, killQueueCall};
}
static void prependStmtsp(AstNodeFTask* const nodep, AstNode* const stmtp) {
static void prependStmtsp(AstBegin* 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 +259,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 +316,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 +341,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<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.
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 +386,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 +580,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 +588,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<AstBegin*> 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 +601,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.at(beginp);
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 "

View File

@ -749,15 +749,21 @@ class TimingControlVisitor final : public VNVisitor {
addDebugInfo(donep);
beginp->addStmtsp(donep->makeStmt());
}
static bool hasDisableQueuePushSelfPrefix(const AstBegin* const beginp) {
static bool hasDisableQueuePushSelfPrefix(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();
// V3LiftExpr lifts std::process::self() into an assignment to a temporary, then V3Task
// lowers that assignment's function call into a leading comment and AstStmtExpr. Thus the
// push_back is no longer necessarily the first statement. Scan across this generated
// prefix for it, stopping at the fork-start sentinel or the branch body. Registrations for
// nested forks live in sub-blocks and so are not in this statement list.
for (AstNode* stmtp = beginp->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
if (VN_IS(stmtp, Comment)) continue;
AstStmtExpr* const stmtExprp = VN_CAST(stmtp, StmtExpr);
if (!stmtExprp) break;
if (stmtExprp->isDisableQueuePushSelfStmt()) 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 {

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -0,0 +1,171 @@
// 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..join / fork..join_any block from one of its own
// branches must terminate the sibling branches, including any that are
// suspended on a delay. This is the join/join_any analogue of the
// fork..join_none cases in t_disable_inside (#6591): the bug was that a sibling
// spawned later in source order registered its process for kill only after the
// disabling branch had already issued the kill, so the sibling survived and its
// $stop fired one time unit later.
// 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);
// verilog_format: on
module t;
// fork..join: the disabling branch comes first, the delayed sibling second.
initial begin
fork : fork_blk2
begin
`checkt($time, 0);
disable fork_blk2;
$stop;
end
begin
`checkt($time, 0);
#1 $stop;
end
join
end
// fork..join: the delayed sibling comes first, the disabling branch second.
initial begin
fork : fork_blk3
begin
`checkt($time, 0);
#1 $stop;
end
begin
`checkt($time, 0);
disable fork_blk3;
$stop;
end
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
begin
`checkt($time, 0);
disable fork_blk2a;
$stop;
end
begin
`checkt($time, 0);
#1 $stop;
end
join_any
end
// fork..join_any: the delayed sibling comes first, the disabling branch second.
initial begin
fork : fork_blk3a
begin
`checkt($time, 0);
#1 $stop;
end
begin
`checkt($time, 0);
disable fork_blk3a;
$stop;
end
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
fork : fork_blk4
begin
`checkt($time, 0);
if ($c("false")) begin
disable fork_blk4;
$stop;
end
#1;
`checkt($time, 1);
end
begin
`checkt($time, 0);
#1;
`checkt($time, 1);
end
join
end
// Deeply nested fork..join: the named fork is several levels deep inside other
// forks and one of its own branches disables it. The delayed sibling at that
// deep level must still be killed, so the fix has to work when the disable-able
// fork is itself nested inside other (suspending) forks.
initial begin
fork
begin
fork
begin
fork : fork_deep
begin
`checkt($time, 0);
disable fork_deep;
$stop;
end
begin
`checkt($time, 0);
#1 $stop;
end
join
end
join
end
join
end
initial begin
#10;
$write("*-* All Finished *-*\n");
$finish;
end
endmodule

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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