Support disable task by name (#6853) (#7136)

This commit is contained in:
Nick Brereton 2026-03-23 22:56:31 -04:00 committed by GitHub
parent 716b404256
commit 3b328d2bb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1168 additions and 149 deletions

View File

@ -181,8 +181,8 @@ package std;
static task killQueue(ref process processQueue[$]);
`ifdef VERILATOR_TIMING
while (processQueue.size() > 0) begin
processQueue.pop_back().kill();
repeat (processQueue.size()) begin
processQueue.pop_front().kill();
end
`endif
endtask

View File

@ -238,10 +238,56 @@ void VlDynamicTriggerScheduler::dump() const {
//======================================================================
// VlForkSync:: Methods
void VlForkSync::done(const char* filename, int lineno) {
void VlProcess::forkSyncOnKill(VlForkSyncState* forkSyncp) {
m_forkSyncOnKillp = forkSyncp;
m_forkSyncOnKillDone = false;
}
void VlProcess::forkSyncOnKillClear(VlForkSyncState* forkSyncp) {
if (m_forkSyncOnKillp != forkSyncp) return;
m_forkSyncOnKillp = nullptr;
m_forkSyncOnKillDone = false;
}
void VlProcess::state(int s) {
if (s == KILLED && m_state != KILLED && m_state != FINISHED && m_forkSyncOnKillp
&& !m_forkSyncOnKillDone) {
m_forkSyncOnKillDone = true;
m_state = s;
m_forkSyncOnKillp->done();
return;
}
m_state = s;
}
VlForkSyncState::~VlForkSyncState() {
for (const VlProcessRef& processp : m_onKillProcessps) processp->forkSyncOnKillClear(this);
}
void VlForkSync::onKill(VlProcessRef process) {
if (!process) return;
m_state->m_onKillProcessps.emplace_back(process);
process->forkSyncOnKill(m_state.get());
}
void VlForkSyncState::done(const char* filename, int lineno) {
VL_DEBUG_IF(VL_DBG_MSGF(" Process forked at %s:%d finished\n", filename, lineno););
if (m_join->m_counter > 0) m_join->m_counter--;
if (m_join->m_counter == 0) m_join->m_susp.resume();
if (!m_inited) {
++m_pendingDones;
return;
}
if (m_counter > 0) m_counter--;
if (m_counter != 0) return;
if (m_inDone) {
m_resumePending = true;
return;
}
m_inDone = true;
do {
m_resumePending = false;
m_susp.resume();
} while (m_resumePending && m_inited && m_counter == 0);
m_inDone = false;
}
//======================================================================

View File

@ -383,40 +383,59 @@ struct VlForever final {
//=============================================================================
// VlForkSync is used to manage fork..join and fork..join_any constructs.
class VlForkSync final {
// VlJoin stores the handle of a suspended coroutine that did a fork..join or fork..join_any.
// If the counter reaches 0, the suspended coroutine shall be resumed.
struct VlJoin final {
size_t m_counter = 0; // When reaches 0, resume suspended coroutine
VlCoroutineHandle m_susp; // Coroutine to resume
};
// Shared fork..join state, because VlForkSync is copied into generated coroutine frames.
class VlForkSyncState final {
public:
size_t m_counter = 0; // When reaches 0, resume suspended coroutine
VlCoroutineHandle m_susp; // Coroutine to resume
bool m_inited = false;
size_t m_pendingDones = 0; // done() calls seen before init() (e.g. early killed branch)
bool m_inDone = false; // Guard against re-entrant resume recursion from nested kills
bool m_resumePending = false; // Join reached zero again while inside done()
std::vector<VlProcessRef> m_onKillProcessps; // Branches registered for kill hooks
// The join info is shared among all forked processes
std::shared_ptr<VlJoin> m_join;
VlForkSyncState() // Construct with a null coroutine handle
: m_susp{VlProcessRef{}} {}
~VlForkSyncState();
void done(const char* filename = VL_UNKNOWN, int lineno = 0);
};
class VlForkSync final {
std::shared_ptr<VlForkSyncState> m_state{std::make_shared<VlForkSyncState>()};
public:
// Create the join object and set the counter to the specified number
void init(size_t count, VlProcessRef process) { m_join.reset(new VlJoin{count, {process}}); }
void init(size_t count, VlProcessRef process) {
const size_t pendingDones = m_state->m_pendingDones;
m_state->m_pendingDones = 0;
count = (pendingDones >= count) ? 0 : (count - pendingDones);
m_state->m_counter = count;
m_state->m_susp = {process};
m_state->m_inited = true;
}
// Register process kill callback so killed fork branches still decrement join counter
void onKill(VlProcessRef process);
// Called whenever any of the forked processes finishes. If the join counter reaches 0, the
// main process gets resumed
void done(const char* filename = VL_UNKNOWN, int lineno = 0);
void done(const char* filename = VL_UNKNOWN, int lineno = 0) {
m_state->done(filename, lineno);
}
// Used by coroutines for co_awaiting a join
auto join(VlProcessRef process, const char* filename = VL_UNKNOWN, int lineno = 0) {
assert(m_join);
VL_DEBUG_IF(
VL_DBG_MSGF(" Awaiting join of fork at: %s:%d\n", filename, lineno););
struct Awaitable final {
VlProcessRef process; // Data of the suspended process, null if not needed
const std::shared_ptr<VlJoin> join; // Join to await on
const std::shared_ptr<VlForkSyncState> state; // Join to await on
VlFileLineDebug fileline;
bool await_ready() { return join->m_counter == 0; } // Suspend if join still exists
bool await_ready() { return state->m_counter == 0; } // Suspend if join still exists
void await_suspend(std::coroutine_handle<> coro) {
join->m_susp = {coro, process, fileline};
state->m_susp = {coro, process, fileline};
}
void await_resume() const {}
};
return Awaitable{process, m_join, VlFileLineDebug{filename, lineno}};
return Awaitable{process, m_state, VlFileLineDebug{filename, lineno}};
}
};

View File

@ -109,12 +109,17 @@ constexpr IData VL_CLOG2_CE_Q(QData lhs) VL_PURE {
// Metadata of processes
using VlProcessRef = std::shared_ptr<VlProcess>;
class VlForkSync;
class VlForkSyncState;
class VlProcess final {
// MEMBERS
int m_state; // Current state of the process
VlProcessRef m_parentp = nullptr; // Parent process, if exists
std::set<VlProcess*> m_children; // Active child processes
VlForkSyncState* m_forkSyncOnKillp
= nullptr; // Optional fork..join counter to decrement on kill
bool m_forkSyncOnKillDone = false; // Ensure on-kill callback fires only once
public:
// TYPES
@ -145,14 +150,18 @@ public:
void detach(VlProcess* childp) { m_children.erase(childp); }
int state() const { return m_state; }
void state(int s) { m_state = s; }
void state(int s);
void disable() {
state(KILLED);
disableFork();
}
void disableFork() {
for (VlProcess* childp : m_children) childp->disable();
// childp->disable() may resume coroutines and mutate m_children
const std::set<VlProcess*> children = m_children;
for (VlProcess* childp : children) childp->disable();
}
void forkSyncOnKill(VlForkSyncState* forkSyncp);
void forkSyncOnKillClear(VlForkSyncState* forkSyncp);
bool completed() const { return state() == FINISHED || state() == KILLED; }
bool completedFork() const {
for (const VlProcess* const childp : m_children)

View File

@ -815,6 +815,7 @@ public:
FORK_DONE,
FORK_INIT,
FORK_JOIN,
FORK_ON_KILL,
RANDOMIZER_BASIC_STD_RANDOMIZATION,
RANDOMIZER_CLEARCONSTRAINTS,
RANDOMIZER_CLEARALL,
@ -959,6 +960,7 @@ inline std::ostream& operator<<(std::ostream& os, const VCMethod& rhs) {
{FORK_DONE, "done", false}, \
{FORK_INIT, "init", false}, \
{FORK_JOIN, "join", false}, \
{FORK_ON_KILL, "onKill", false}, \
{RANDOMIZER_BASIC_STD_RANDOMIZATION, "basicStdRandomization", false}, \
{RANDOMIZER_CLEARCONSTRAINTS, "clearConstraints", false}, \
{RANDOMIZER_CLEARALL, "clearAll", false}, \

View File

@ -597,6 +597,39 @@ class ForkVisitor final : public VNVisitor {
// We did wrap the body
return true;
}
static bool isForkJoinNoneSentinelDelay(const AstNode* const nodep) {
const AstDelay* const delayp = VN_CAST(nodep, Delay);
if (!delayp) return false;
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->name().rfind("__VprocessQueue_", 0) == 0;
}
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 delayp = firstStmtp->unlinkFrBack();
if (insertBeforep) {
insertBeforep->addHereThisAsNext(delayp);
} else {
beginp->addStmtsp(delayp);
}
}
// VISITORS
void visit(AstNodeModule* nodep) override {
@ -634,6 +667,7 @@ class ForkVisitor final : public VNVisitor {
new AstConst{fl, AstConst::Unsized64{}, std::numeric_limits<uint64_t>::max()},
false};
itemp->stmtsp()->addHereThisAsNext(delayp);
moveForkSentinelAfterDisableQueuePushes(itemp);
}
}

View File

@ -37,6 +37,7 @@
#include "V3Error.h"
#include "V3UniqueNames.h"
#include <unordered_map>
#include <vector>
VL_DEFINE_DEBUG_FUNCTIONS;
@ -65,6 +66,12 @@ class LinkJumpVisitor final : public VNVisitor {
std::vector<AstNodeBlock*> m_blockStack; // All begin blocks above current node
V3UniqueNames m_queueNames{
"__VprocessQueue"}; // Names for queues needed for 'disable' handling
std::unordered_map<const AstTask*, AstVar*> m_taskDisableQueues; // Per-task process queues
std::unordered_map<const AstBegin*, AstVar*> m_beginDisableQueues; // Per-begin process queues
std::unordered_map<const AstTask*, AstBegin*>
m_taskDisableBegins; // Per-task process wrappers
std::unordered_map<const AstBegin*, AstBegin*>
m_beginDisableBegins; // Per-begin process wrappers
// METHODS
// Get (and create if necessary) the JumpBlock for this statement
@ -177,6 +184,123 @@ class LinkJumpVisitor final : public VNVisitor {
return new AstStmtExpr{
fl, new AstMethodCall{fl, queueRefp, "push_back", new AstArg{fl, "", processSelfp}}};
}
static AstStmtExpr* getQueuePushProcessSelfp(FileLine* const fl, AstVar* const processQueuep) {
AstPackage* const topPkgp = v3Global.rootp()->dollarUnitPkgAddp();
AstVarRef* const queueWriteRefp
= new AstVarRef{fl, topPkgp, processQueuep, VAccess::WRITE};
return getQueuePushProcessSelfp(queueWriteRefp);
}
static AstStmtExpr* getQueueKillStmtp(FileLine* const fl, AstVar* const processQueuep) {
AstPackage* const topPkgp = v3Global.rootp()->dollarUnitPkgAddp();
AstClass* const processClassp
= VN_AS(getMemberp(v3Global.rootp()->stdPackagep(), "process"), Class);
AstVarRef* const queueRefp = new AstVarRef{fl, topPkgp, processQueuep, VAccess::READWRITE};
AstTaskRef* const killQueueCall
= new AstTaskRef{fl, VN_AS(getMemberp(processClassp, "killQueue"), Task),
new AstArg{fl, "", queueRefp}};
killQueueCall->classOrPackagep(processClassp);
return new AstStmtExpr{fl, killQueueCall};
}
static void prependStmtsp(AstNodeFTask* const nodep, AstNode* const stmtp) {
if (AstNode* const origStmtsp = nodep->stmtsp()) {
origStmtsp->unlinkFrBackWithNext();
stmtp->addNext(origStmtsp);
}
nodep->addStmtsp(stmtp);
}
static void prependStmtsp(AstNodeBlock* const nodep, AstNode* const stmtp) {
if (AstNode* const origStmtsp = nodep->stmtsp()) {
origStmtsp->unlinkFrBackWithNext();
stmtp->addNext(origStmtsp);
}
nodep->addStmtsp(stmtp);
}
static bool directlyUnderFork(const AstNode* const nodep) {
if (nodep->backp()->nextp() == nodep) return directlyUnderFork(nodep->backp());
return VN_IS(nodep->backp(), Fork);
}
AstBegin* getOrCreateTaskDisableBeginp(AstTask* const taskp, FileLine* const fl) {
const auto it = m_taskDisableBegins.find(taskp);
if (it != m_taskDisableBegins.end()) return it->second;
AstBegin* const taskBodyp = new AstBegin{fl, "", nullptr, false};
// Disable-by-name rewrites kill this detached task-body process, so mark it as process
// backed to ensure fork/join kill-accounting hooks are always emitted.
taskBodyp->setNeedProcess();
if (taskp->stmtsp()) taskBodyp->addStmtsp(taskp->stmtsp()->unlinkFrBackWithNext());
AstFork* const forkp = new AstFork{fl, VJoinType::JOIN};
forkp->addForksp(taskBodyp);
taskp->addStmtsp(forkp);
m_taskDisableBegins.emplace(taskp, taskBodyp);
return taskBodyp;
}
AstVar* getOrCreateTaskDisableQueuep(AstTask* const taskp, FileLine* const fl) {
const auto it = m_taskDisableQueues.find(taskp);
if (it != m_taskDisableQueues.end()) return it->second;
AstPackage* const topPkgp = v3Global.rootp()->dollarUnitPkgAddp();
AstClass* const processClassp
= VN_AS(getMemberp(v3Global.rootp()->stdPackagep(), "process"), Class);
AstVar* const processQueuep = new AstVar{
fl, VVarType::VAR, m_queueNames.get(taskp->name()), VFlagChildDType{},
new AstQueueDType{fl, VFlagChildDType{},
new AstClassRefDType{fl, processClassp, nullptr}, nullptr}};
processQueuep->lifetime(VLifetime::STATIC_EXPLICIT);
topPkgp->addStmtsp(processQueuep);
AstStmtExpr* const pushCurrentProcessp = getQueuePushProcessSelfp(fl, processQueuep);
AstBegin* const taskBodyp = getOrCreateTaskDisableBeginp(taskp, fl);
prependStmtsp(taskBodyp, pushCurrentProcessp);
m_taskDisableQueues.emplace(taskp, processQueuep);
return processQueuep;
}
AstBegin* getOrCreateBeginDisableBeginp(AstBegin* const beginp, FileLine* const fl) {
const auto it = m_beginDisableBegins.find(beginp);
if (it != m_beginDisableBegins.end()) return it->second;
AstBegin* const beginBodyp = new AstBegin{fl, "", nullptr, false};
if (beginp->stmtsp()) beginBodyp->addStmtsp(beginp->stmtsp()->unlinkFrBackWithNext());
AstFork* const forkp = new AstFork{fl, VJoinType::JOIN};
forkp->addForksp(beginBodyp);
beginp->addStmtsp(forkp);
m_beginDisableBegins.emplace(beginp, beginBodyp);
return beginBodyp;
}
AstVar* getOrCreateBeginDisableQueuep(AstBegin* const beginp, FileLine* const fl) {
const auto it = m_beginDisableQueues.find(beginp);
if (it != m_beginDisableQueues.end()) return it->second;
AstPackage* const topPkgp = v3Global.rootp()->dollarUnitPkgAddp();
AstClass* const processClassp
= VN_AS(getMemberp(v3Global.rootp()->stdPackagep(), "process"), Class);
AstVar* const processQueuep = new AstVar{
fl, VVarType::VAR, m_queueNames.get(beginp->name()), VFlagChildDType{},
new AstQueueDType{fl, VFlagChildDType{},
new AstClassRefDType{fl, processClassp, nullptr}, nullptr}};
processQueuep->lifetime(VLifetime::STATIC_EXPLICIT);
topPkgp->addStmtsp(processQueuep);
AstStmtExpr* const pushCurrentProcessp = getQueuePushProcessSelfp(fl, processQueuep);
AstBegin* const beginBodyp = getOrCreateBeginDisableBeginp(beginp, fl);
prependStmtsp(beginBodyp, pushCurrentProcessp);
// Named-block disable must also terminate detached descendants created by forks
// under the block, so track each fork branch process in the same queue.
beginBodyp->foreach([&](AstFork* const forkp) {
for (AstBegin* branchp = forkp->forksp(); branchp;
branchp = VN_AS(branchp->nextp(), Begin)) {
AstStmtExpr* const pushBranchProcessp
= getQueuePushProcessSelfp(fl, processQueuep);
prependStmtsp(branchp, pushBranchProcessp);
}
});
m_beginDisableQueues.emplace(beginp, processQueuep);
return processQueuep;
}
void handleDisableOnFork(AstDisable* const nodep, const std::vector<AstBegin*>& forks) {
// The support utilizes the process::kill()` method. For each `disable` a queue of
// processes is declared. At the beginning of each fork that can be disabled, its process
@ -210,17 +334,9 @@ class LinkJumpVisitor final : public VNVisitor {
if (pushCurrentProcessp->backp()) {
pushCurrentProcessp = pushCurrentProcessp->cloneTree(false);
}
if (beginp->stmtsp()) {
// There is no need to add it to empty block
beginp->stmtsp()->addHereThisAsNext(pushCurrentProcessp);
}
prependStmtsp(beginp, pushCurrentProcessp);
}
AstVarRef* const queueRefp = new AstVarRef{fl, topPkgp, processQueuep, VAccess::READWRITE};
AstTaskRef* const killQueueCall
= new AstTaskRef{fl, VN_AS(getMemberp(processClassp, "killQueue"), Task),
new AstArg{fl, "", queueRefp}};
killQueueCall->classOrPackagep(processClassp);
AstStmtExpr* const killStmtp = new AstStmtExpr{fl, killQueueCall};
AstStmtExpr* const killStmtp = getQueueKillStmtp(fl, processQueuep);
nodep->addNextHere(killStmtp);
// 'process::kill' does not immediately kill the current process
@ -239,12 +355,6 @@ class LinkJumpVisitor final : public VNVisitor {
}
}
}
static bool directlyUnderFork(const AstNode* const nodep) {
if (nodep->backp()->nextp() == nodep) return directlyUnderFork(nodep->backp());
if (VN_IS(nodep->backp(), Fork)) return true;
return false;
}
// VISITORS
void visit(AstNodeModule* nodep) override {
if (nodep->dead()) return;
@ -417,9 +527,26 @@ class LinkJumpVisitor final : public VNVisitor {
void visit(AstDisable* nodep) override {
UINFO(8, " DISABLE " << nodep);
AstNode* const targetp = nodep->targetp();
UASSERT_OBJ(targetp, nodep, "Unlinked disable statement");
if (VN_IS(targetp, Task)) {
nodep->v3warn(E_UNSUPPORTED, "Unsupported: disabling task by name");
if (!targetp) {
// Linking errors on the disable target are already reported upstream.
// Drop this node to avoid cascading into an internal assertion.
VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep);
return;
}
if (AstTask* const taskp = VN_CAST(targetp, Task)) {
AstVar* const processQueuep = getOrCreateTaskDisableQueuep(taskp, nodep->fileline());
AstStmtExpr* const killStmtp = getQueueKillStmtp(nodep->fileline(), processQueuep);
nodep->addNextHere(killStmtp);
// process::kill does not terminate the currently running process immediately.
// If we disable the current task by name from inside itself, jump to its end.
if (m_ftaskp == taskp) {
AstNode* jumpTargetp = taskp;
const auto it = m_taskDisableBegins.find(taskp);
if (it != m_taskDisableBegins.end()) jumpTargetp = it->second;
AstJumpBlock* const blockp = getJumpBlock(jumpTargetp, false);
killStmtp->addNextHere(new AstJumpGo{nodep->fileline(), blockp});
}
} else if (AstFork* const forkp = VN_CAST(targetp, Fork)) {
std::vector<AstBegin*> forks;
for (AstBegin* itemp = forkp->forksp(); itemp; itemp = VN_AS(itemp->nextp(), Begin)) {
@ -428,22 +555,37 @@ class LinkJumpVisitor final : public VNVisitor {
handleDisableOnFork(nodep, forks);
} else if (AstBegin* const beginp = VN_CAST(targetp, Begin)) {
if (existsBlockAbove(beginp->name())) {
if (beginp->user3()) {
nodep->v3warn(E_UNSUPPORTED,
"Unsupported: disabling block that contains a fork");
} else {
if (!beginp->user3()) {
// Jump to the end of the named block
AstJumpBlock* const blockp = getJumpBlock(beginp, false);
nodep->addNextHere(new AstJumpGo{nodep->fileline(), blockp});
} else {
AstVar* const processQueuep
= getOrCreateBeginDisableQueuep(beginp, nodep->fileline());
AstStmtExpr* const killStmtp
= getQueueKillStmtp(nodep->fileline(), processQueuep);
nodep->addNextHere(killStmtp);
// process::kill does not terminate the currently running process immediately.
// If disable executes inside a fork branch of this named block, jump to the
// end of that branch to prevent statements after disable from executing.
AstBegin* currentBeginp = nullptr;
for (AstNodeBlock* const blockp : vlstd::reverse_view(m_blockStack)) {
if (VN_IS(blockp, Begin)) {
currentBeginp = VN_AS(blockp, Begin);
break;
}
}
if (currentBeginp && directlyUnderFork(currentBeginp)) {
AstJumpBlock* const blockp = getJumpBlock(currentBeginp, false);
killStmtp->addNextHere(new AstJumpGo{nodep->fileline(), blockp});
}
}
} else {
if (directlyUnderFork(beginp)) {
std::vector<AstBegin*> forks{beginp};
handleDisableOnFork(nodep, forks);
} else {
nodep->v3warn(E_UNSUPPORTED, "disable isn't underneath a begin with name: '"
<< beginp->name() << "'");
}
AstVar* const processQueuep
= getOrCreateBeginDisableQueuep(beginp, nodep->fileline());
AstStmtExpr* const killStmtp = getQueueKillStmtp(nodep->fileline(), processQueuep);
nodep->addNextHere(killStmtp);
}
} else {
nodep->v3fatalSrc("Disable linked with node of unhandled type "

View File

@ -742,6 +742,34 @@ class TimingControlVisitor final : public VNVisitor {
addDebugInfo(donep);
beginp->addStmtsp(donep->makeStmt());
}
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->name().rfind("__VprocessQueue_", 0) == 0;
}
// Register a callback so killing a process-backed fork branch decrements the join counter
void addForkOnKill(AstBegin* const beginp, AstVarScope* const forkVscp) const {
if (!beginp->needProcess() && !hasDisableQueuePushSelfPrefix(beginp)) return;
FileLine* const flp = beginp->fileline();
AstCMethodHard* const onKillp = new AstCMethodHard{
flp, new AstVarRef{flp, forkVscp, VAccess::WRITE}, VCMethod::FORK_ON_KILL};
onKillp->dtypeSetVoid();
AstCExpr* const processp = new AstCExpr{flp, "vlProcess"};
processp
->dtypeSetVoid(); // Opaque process reference; type is irrelevant for hardcoded emit
onKillp->addPinsp(processp);
AstNodeStmt* const stmtp = onKillp->makeStmt();
if (beginp->stmtsp()) {
beginp->stmtsp()->addHereThisAsNext(stmtp);
} else {
beginp->addStmtsp(stmtp);
}
}
// Handle the 'join' part of a fork..join
void makeForkJoin(AstFork* const forkp) {
// Create a fork sync var
@ -754,6 +782,7 @@ class TimingControlVisitor final : public VNVisitor {
unsigned joinCount = 0; // Needed for join counter
// Add a <fork sync>.done() to each begin
for (AstNode* beginp = forkp->forksp(); beginp; beginp = beginp->nextp()) {
addForkOnKill(VN_AS(beginp, Begin), forkVscp);
addForkDone(VN_AS(beginp, Begin), forkVscp);
joinCount++;
}

View File

@ -1,5 +0,0 @@
%Error-UNSUPPORTED: t/t_disable.v:17:9: Unsupported: disabling block that contains a fork
17 | disable forked;
| ^~~~~~~
... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest
%Error: Exiting due to

View File

@ -9,8 +9,10 @@
import vltest_bootstrap
test.scenarios('linter')
test.scenarios('simulator')
test.lint(fails=True, expect_filename=test.golden_filename)
test.compile(timing_loop=True, verilator_flags2=["--timing"])
test.execute()
test.passes()

View File

@ -2,7 +2,4 @@
9 | disable abcd;
| ^~~~
... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance.
%Error: Internal Error: t/t_disable_bad.v:9:5: ../V3LinkJump.cpp:#: Unlinked disable statement
9 | disable abcd;
| ^~~~~~~
... This fatal error may be caused by the earlier error(s); resolve those first.
%Error: Exiting due to

View File

@ -1,5 +0,0 @@
%Error-UNSUPPORTED: t/t_disable_empty.v:12:5: disable isn't underneath a begin with name: 'block'
12 | disable block;
| ^~~~~~~
... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest
%Error: Exiting due to

View File

@ -11,6 +11,8 @@ import vltest_bootstrap
test.scenarios('simulator')
test.lint(fails=True, expect_filename=test.golden_filename)
test.compile(verilator_flags2=["--binary"])
test.execute()
test.passes()

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(timing_loop=True, verilator_flags2=["--timing"])
test.execute()
test.passes()

View File

@ -0,0 +1,436 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// 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
int x = 0;
int y = 0;
int z = 0;
int always_value = 0;
int self_entry = 0;
int self_after_disable = 0;
int par_started = 0;
int par_finished = 0;
int par_parent_continued = 0;
int named_block_fork_fail = 0;
int task_named_block_fork_fail = 0;
event named_block_fork_ev;
event task_named_block_fork_ev;
class StaticCls;
static int ticks = 0;
static task run();
forever begin
#10;
ticks++;
end
endtask
static task stop();
disable run;
endtask
endclass
task increment_x;
x++;
#2;
x++;
endtask
task increment_y;
y++;
#5;
y++;
endtask
task finish_z;
z++;
endtask
task always_foo;
always_value = #2 1;
endtask
task self_stop;
self_entry = 1;
disable self_stop;
self_after_disable = 1;
endtask
task worker;
par_started++;
#20;
par_finished++;
endtask
task parent_disables_worker;
fork
worker();
worker();
join_none
#5;
disable worker;
par_parent_continued = 1;
endtask
task task_named_block_fork_disable;
begin : t_named_block
fork
begin
#5;
task_named_block_fork_fail = 1;
end
begin
@task_named_block_fork_ev;
disable t_named_block;
task_named_block_fork_fail = 2;
end
join
task_named_block_fork_fail = 3;
end
endtask
class Cls;
int m_time = 0;
task get_and_send();
forever begin
#10;
m_time += 10;
end
endtask
task post_shutdown_phase();
disable get_and_send;
endtask
endclass
class NamedA;
int v = 0;
task run();
forever begin
#10;
v++;
end
endtask
task stop();
disable run;
endtask
endclass
class NamedB;
int v = 0;
task run();
forever begin
#10;
v++;
end
endtask
endclass
class BaseNamed;
int v = 0;
task run();
forever begin
#10;
v++;
end
endtask
endclass
class ChildNamed extends BaseNamed;
int child_v = 0;
task run();
forever begin
#10;
child_v++;
end
endtask
task stop();
disable run;
endtask
endclass
package PkgRun;
int v = 0;
task run();
forever begin
#10;
v++;
end
endtask
task stop();
disable run;
endtask
endpackage
interface Ifc;
int v = 0;
task run();
forever begin
#10;
v++;
end
endtask
task stop();
disable run;
endtask
endinterface
program Prog;
int v = 0;
task run();
forever begin
#10;
v++;
end
endtask
task stop();
disable run;
endtask
endprogram
module WorkerMod;
int m = 0;
task run();
forever begin
#10;
m++;
end
endtask
endmodule
module t;
Ifc ifc1();
Prog prog1();
WorkerMod mod1();
always #6 disable always_foo;
initial begin
automatic Cls c = new;
automatic NamedA a = new;
automatic NamedB b = new;
automatic BaseNamed base_obj = new;
automatic ChildNamed child_obj = new;
int m_time_before_shutdown;
int mod1_before;
int a_before;
int b_before;
int static_before;
int base_before;
int child_before;
int ifc_before;
int prog_before;
int pkg_before;
// Disable task by name from an external always block while task executes
always_value = 0;
#5;
always_foo;
#4;
if (always_value != 0) $stop;
// Module task disabled by sibling process in a fork
fork
increment_x();
#1 disable increment_x;
join_none
#10;
if (x != 1) $stop;
// Re-disabling after prior disable (no active invocations) is a no-op
disable increment_x;
#1;
if (x != 1) $stop;
// Another basic module-task disable-by-name case
fork
increment_y();
#3 disable increment_y;
join_none
#10;
if (y != 1) $stop;
// Disable named block containing fork from inside a fork branch
fork
begin : named_block_under_test
fork
begin
#5;
named_block_fork_fail = 1;
end
begin
@named_block_fork_ev;
disable named_block_under_test;
named_block_fork_fail = 2;
end
join
named_block_fork_fail = 3;
end
join_none
#2;
->named_block_fork_ev;
#10;
if (named_block_fork_fail != 0) $stop;
// Same case as above, but with the named block inside a task
fork
task_named_block_fork_disable();
join_none
#2;
->task_named_block_fork_ev;
#10;
if (task_named_block_fork_fail != 0) $stop;
// Disabling a task after it already finished is a no-op
finish_z();
if (z != 1) $stop;
disable finish_z;
#1;
if (z != 1) $stop;
// Self-disable in task by name
self_stop();
if (self_entry != 1) $stop;
if (self_after_disable != 0) $stop;
// Same task launched in parallel, disabled from parent task context
parent_disables_worker();
#30;
if (par_started != 2) $stop;
if (par_finished != 0) $stop;
if (par_parent_continued != 1) $stop;
// Same task launched in parallel, disabled from sibling process context
par_started = 0;
par_finished = 0;
fork
worker();
worker();
begin
#5;
disable worker;
end
join_none
#30;
if (par_started != 2) $stop;
if (par_finished != 0) $stop;
// Static class task disabled by name from another static task
fork
StaticCls::run();
StaticCls::run();
join_none
#25;
if (StaticCls::ticks == 0) $stop;
static_before = StaticCls::ticks;
StaticCls::stop();
#30;
if (StaticCls::ticks != static_before) $stop;
// Same task name in different class scopes: disable only one scope
fork
a.run();
b.run();
join_none
#25;
if (a.v == 0 || b.v == 0) $stop;
a_before = a.v;
b_before = b.v;
a.stop();
#30;
if (a.v != a_before) $stop;
if (b.v <= b_before) $stop;
// Same task name across inheritance scopes: disable only derived task
fork
base_obj.run();
child_obj.run();
join_none
#25;
if (base_obj.v == 0 || child_obj.child_v == 0) $stop;
base_before = base_obj.v;
child_before = child_obj.child_v;
child_obj.stop();
#30;
if (child_obj.child_v != child_before) $stop;
if (base_obj.v <= base_before) $stop;
// Interface task disabled by name through interface scope
fork
ifc1.run();
join_none
#25;
if (ifc1.v == 0) $stop;
ifc_before = ifc1.v;
ifc1.stop();
#30;
if (ifc1.v != ifc_before) $stop;
// Program task disabled by name through program scope
fork
prog1.run();
join_none
#25;
if (prog1.v == 0) $stop;
prog_before = prog1.v;
prog1.stop();
#30;
if (prog1.v != prog_before) $stop;
// Package task disabled by name through package scope
fork
PkgRun::run();
join_none
#25;
if (PkgRun::v == 0) $stop;
pkg_before = PkgRun::v;
PkgRun::stop();
#30;
if (PkgRun::v != pkg_before) $stop;
// Dotted hierarchical task disable of module task by instance path
fork
mod1.run();
join_none
#25;
if (mod1.m == 0) $stop;
mod1_before = mod1.m;
disable mod1.run;
#30;
if (mod1.m != mod1_before) $stop;
// Class task disabled by name from outside that task
fork
c.get_and_send();
join_none
#35;
if (c.m_time == 0) $fatal;
m_time_before_shutdown = c.m_time;
c.post_shutdown_phase();
#30;
if (c.m_time != m_time_before_shutdown) $fatal;
$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: 2024-2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('simulator')
test.compile(timing_loop=True, verilator_flags2=["--timing"])
test.execute()
test.passes()

View File

@ -0,0 +1,237 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain
// SPDX-FileCopyrightText: 2025 Antmicro
// SPDX-License-Identifier: CC0-1.0
int x = 0;
int y = 0;
int z = 0;
int w = 0;
int jf = 0;
int ddj = 0;
int ddja = 0;
int ddjn = 0;
int dj_done = 0;
int dja_done = 0;
int djn_done = 0;
int race_disable = 0;
task increment_x;
x++;
#2;
x++;
endtask
task increment_y;
y++;
#2;
y++;
endtask
class driver;
int m_time = 0;
task get_and_send();
forever begin
#10;
m_time += 10;
end
endtask
task post_shutdown_phase();
disable get_and_send;
endtask
endclass
module t;
driver c;
task automatic progress(input string label);
$display("DBG:t_disable_task_join:%s t=%0t x=%0d y=%0d z=%0d w=%0d m_time=%0d",
label, $time, x, y, z, w, (c == null) ? -1 : c.m_time);
endtask
initial begin
progress("start");
fork
increment_x();
#1 disable increment_x;
join
if (x != 1) $fatal(1, "x=%0d expected 1", x);
c = new;
fork
c.get_and_send;
join_none
if (c.m_time != 0) $fatal(1, "c.m_time=%0d expected 0 before first delay", c.m_time);
#11;
if ($time != 12) $fatal(1, "$time=%0t expected 12", $time);
if (c.m_time != 10) $fatal(1, "c.m_time=%0d expected 10 after 11 ticks", c.m_time);
#20;
if ($time != 32) $fatal(1, "$time=%0t expected 32", $time);
if (c.m_time != 30) $fatal(1, "c.m_time=%0d expected 30 before disable", c.m_time);
c.post_shutdown_phase;
#20;
if ($time != 52) $fatal(1, "$time=%0t expected 52", $time);
if (c.m_time != 30) $fatal(1, "c.m_time=%0d expected 30 after disable", c.m_time);
progress("after_class_task_disable");
// Additional regression: join_any should also complete when disable kills a forked task
fork
increment_y();
#1 disable increment_y;
join_any
#3;
if (y != 1) $fatal(1, "y=%0d expected 1", y);
progress("after_join_any_task_disable");
// Additional regression: named-block disable with join
fork
begin : worker_join
z++;
#2;
z++;
end
#1 disable worker_join;
join
if (z != 1) $fatal(1, "z=%0d expected 1", z);
progress("after_named_block_join_disable");
// Additional regression: named-block disable with join_any
fork
begin : worker_join_any
w++;
#2;
w++;
end
#1 disable worker_join_any;
join_any
#3;
if (w != 1) $fatal(1, "w=%0d expected 1", w);
progress("after_named_block_join_any_disable");
// disable fork from inside a join_any branch
fork
begin
fork
begin
#1;
jf = 1;
end
begin
#5;
jf = 99;
end
join_none
#2;
disable fork;
end
begin
#3;
end
join_any
#6;
if (jf != 1) $fatal(1, "jf=%0d expected 1", jf);
progress("after_disable_fork");
// multiple sequential disables of the same target under join
fork
begin : twice_join
#5;
ddj = 99;
end
begin
#1 disable twice_join;
#1 disable twice_join;
end
join
if (ddj != 0) $fatal(1, "ddj=%0d expected 0", ddj);
// multiple sequential disables of the same target under join_any
fork
begin : twice_join_any
#5;
ddja = 99;
end
begin
#1 disable twice_join_any;
#1 disable twice_join_any;
end
join_any
#6;
if (ddja != 0) $fatal(1, "ddja=%0d expected 0", ddja);
// multiple sequential disables of the same target under join_none
begin
fork
begin : twice_join_none
#5;
ddjn = 99;
end
join_none
#1 disable twice_join_none;
#1 disable twice_join_none;
#6;
end
if (ddjn != 0) $fatal(1, "ddjn=%0d expected 0", ddjn);
// disable after target is already finished (join)
fork
begin : done_join
#1;
dj_done = 1;
end
join
disable done_join;
if (dj_done != 1) $fatal(1, "dj_done=%0d expected 1", dj_done);
// disable after target is already finished (join_any)
fork
begin : done_join_any
#1;
dja_done = 1;
end
#2;
join_any
#2;
disable done_join_any;
if (dja_done != 1) $fatal(1, "dja_done=%0d expected 1", dja_done);
// disable after target is already finished (join_none)
begin
fork
begin : done_join_none
#1;
djn_done = 1;
end
join_none
#2;
disable done_join_none;
#1;
end
if (djn_done != 1) $fatal(1, "djn_done=%0d expected 1", djn_done);
// competing disables in the same time slot targeting the same block
fork
begin : race_target
#5;
race_disable = 99;
end
#1 disable race_target;
#1 disable race_target;
join
if (race_disable != 0) $fatal(1, "race_disable=%0d expected 0", race_disable);
progress("after_race_disable");
progress("before_finish");
$write("*-* All Finished *-*\n");
$finish;
end
endmodule

View File

@ -0,0 +1,20 @@
%Error: t/t_disable_task_scope_bad.v:36:18: Can't find definition of 'missing_task' in dotted block/task: 'ifc1.missing_task'
36 | disable ifc1.missing_task;
| ^~~~~~~~~~~~
... Known scopes under 'ifc1': <no instances found>
... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance.
%Error: t/t_disable_task_scope_bad.v:37:19: Can't find definition of 'missing_task' in dotted block/task: 'prog1.missing_task'
37 | disable prog1.missing_task;
| ^~~~~~~~~~~~
... Known scopes under 'prog1': <no instances found>
%Error: t/t_disable_task_scope_bad.v:38:26: Can't find definition of 'missing_task' in dotted block/task: 'outer1.inner.missing_task'
38 | disable outer1.inner.missing_task;
| ^~~~~~~~~~~~
... Known scopes under 'outer1.inner': <no instances found>
%Error: t/t_disable_task_scope_bad.v:39:18: Found definition of 'ifc1.data' as a VAR but expected a block/task
39 | disable ifc1.data;
| ^~~~
%Error: t/t_disable_task_scope_bad.v:40:19: Found definition of 'prog1.data' as a VAR but expected a block/task
40 | disable prog1.data;
| ^~~~
%Error: Exiting due to

View File

@ -0,0 +1,16 @@
#!/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: 2024-2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('linter')
test.lint(fails=True, expect_filename=test.golden_filename)
test.passes()

View File

@ -0,0 +1,42 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// 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
interface Ifc;
task run;
endtask
int data = 0;
endinterface
program Prog;
task run;
endtask
int data = 0;
endprogram
module Inner;
task run;
endtask
endmodule
module Outer;
Inner inner();
endmodule
module t;
Ifc ifc1();
Prog prog1();
Outer outer1();
initial begin
disable ifc1.missing_task;
disable prog1.missing_task;
disable outer1.inner.missing_task;
disable ifc1.data;
disable prog1.data;
end
endmodule

View File

@ -0,0 +1,11 @@
%Error: t/t_disable_task_target_bad.v:23:13: Found definition of 'foo' as a VAR but expected a block/task
23 | disable foo;
| ^~~
... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance.
%Error: t/t_disable_task_target_bad.v:24:13: Found definition of 'c' as a VAR but expected a block/task
24 | disable c.run;
| ^
%Error: t/t_disable_task_target_bad.v:24:15: Can't find definition of block/task: 'run'
24 | disable c.run;
| ^~~
%Error: Exiting due to

View File

@ -4,12 +4,12 @@
# 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: 2024 Wilson Snyder
# SPDX-FileCopyrightText: 2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('simulator')
test.scenarios('linter')
test.lint(fails=True, expect_filename=test.golden_filename)

View File

@ -0,0 +1,26 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// 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
class Cls;
task run;
endtask
endclass
module t;
task foo;
endtask
Cls c;
initial begin
int foo;
c = new;
disable foo;
disable c.run;
end
endmodule

View File

@ -1,8 +0,0 @@
%Error-UNSUPPORTED: t/t_disable_task_unsup.v:37:10: Unsupported: disabling task by name
37 | #1 disable increment_x;
| ^~~~~~~
... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest
%Error-UNSUPPORTED: t/t_disable_task_unsup.v:26:5: Unsupported: disabling task by name
26 | disable get_and_send;
| ^~~~~~~
%Error: Exiting due to

View File

@ -1,65 +0,0 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain
// SPDX-FileCopyrightText: 2025 Antmicro
// SPDX-License-Identifier: CC0-1.0
int x = 0;
task increment_x;
x++;
#2;
x++;
endtask
class driver;
int m_time = 0;
task get_and_send();
forever begin
#10;
m_time += 10;
end
endtask
task post_shutdown_phase();
disable get_and_send;
endtask
endclass
module t;
driver c;
initial begin
fork
increment_x();
#1 disable increment_x;
join
if (x != 1) $stop;
c = new;
fork
c.get_and_send;
join_none
if (c.m_time != 0) $stop;
#11;
if ($time != 12) $stop;
if (c.m_time != 10) $stop;
#20;
if ($time != 32) $stop;
if (c.m_time != 30) $stop;
c.post_shutdown_phase;
#20;
if ($time != 52) $stop;
if (c.m_time != 30) $stop;
$write("*-* All Finished *-*\n");
$finish;
end
endmodule

View File

@ -14,8 +14,4 @@
%Error: t/t_func_return_bad.v:19:5: break isn't underneath a loop
19 | break;
| ^~~~~
%Error-UNSUPPORTED: t/t_func_return_bad.v:22:5: disable isn't underneath a begin with name: 'foo'
22 | disable foo;
| ^~~~~~~
... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest
%Error: Exiting due to