diff --git a/include/verilated_timing.cpp b/include/verilated_timing.cpp index 51dd88b9a..61d66b2f0 100644 --- a/include/verilated_timing.cpp +++ b/include/verilated_timing.cpp @@ -238,6 +238,26 @@ void VlDynamicTriggerScheduler::dump() const { //====================================================================== // VlForkSync:: Methods +void VlProcess::forkSyncOnKill(VlForkSync* forkSyncp) { + m_forkSyncOnKillp = forkSyncp; + 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; +} + +void VlForkSync::onKill(VlProcessRef process) { + if (process) process->forkSyncOnKill(this); +} + void VlForkSync::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--; diff --git a/include/verilated_timing.h b/include/verilated_timing.h index a657ac458..4eb07402d 100644 --- a/include/verilated_timing.h +++ b/include/verilated_timing.h @@ -397,6 +397,8 @@ class VlForkSync final { 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}}); } + // 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); diff --git a/include/verilated_types.h b/include/verilated_types.h index da8c94977..487b7066e 100644 --- a/include/verilated_types.h +++ b/include/verilated_types.h @@ -109,12 +109,15 @@ constexpr IData VL_CLOG2_CE_Q(QData lhs) VL_PURE { // Metadata of processes using VlProcessRef = std::shared_ptr; +class VlForkSync; class VlProcess final { // MEMBERS int m_state; // Current state of the process VlProcessRef m_parentp = nullptr; // Parent process, if exists std::set m_children; // Active child processes + VlForkSync* 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,7 +148,7 @@ 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(); @@ -153,6 +156,7 @@ public: void disableFork() { for (VlProcess* childp : m_children) childp->disable(); } + void forkSyncOnKill(VlForkSync* forkSyncp); bool completed() const { return state() == FINISHED || state() == KILLED; } bool completedFork() const { for (const VlProcess* const childp : m_children) diff --git a/src/V3AstAttr.h b/src/V3AstAttr.h index e879adfed..e6e82c0d6 100644 --- a/src/V3AstAttr.h +++ b/src/V3AstAttr.h @@ -811,6 +811,7 @@ public: FORK_DONE, FORK_INIT, FORK_JOIN, + FORK_ON_KILL, RANDOMIZER_BASIC_STD_RANDOMIZATION, RANDOMIZER_CLEARCONSTRAINTS, RANDOMIZER_CLEARALL, @@ -947,6 +948,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}, \ diff --git a/src/V3Timing.cpp b/src/V3Timing.cpp index b723cbcb9..a529b4411 100644 --- a/src/V3Timing.cpp +++ b/src/V3Timing.cpp @@ -742,6 +742,23 @@ class TimingControlVisitor final : public VNVisitor { addDebugInfo(donep); beginp->addStmtsp(donep->makeStmt()); } + // 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()) return; + FileLine* const flp = beginp->fileline(); + auto* 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 +771,7 @@ class TimingControlVisitor final : public VNVisitor { unsigned joinCount = 0; // Needed for join counter // Add a .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++; } diff --git a/test_regress/t/t_disable_task_join.py b/test_regress/t/t_disable_task_join.py new file mode 100644 index 000000000..6f8da8aa7 --- /dev/null +++ b/test_regress/t/t_disable_task_join.py @@ -0,0 +1,19 @@ +#!/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() + diff --git a/test_regress/t/t_disable_task_join.v b/test_regress/t/t_disable_task_join.v new file mode 100644 index 000000000..db8763baa --- /dev/null +++ b/test_regress/t/t_disable_task_join.v @@ -0,0 +1,80 @@ +// 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; + +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; + + 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; + + // 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) $stop; + + $write("*-* All Finished *-*\n"); + $finish; + end + +endmodule