Fix nonblocking event trigger never firing in suspendable processes (#8485)

This commit is contained in:
Marco Bartoli
2026-09-29 16:48:09 -04:00
committed by GitHub
parent 21bae44b03
commit 68439fbb72
7 changed files with 293 additions and 12 deletions
+38 -12
View File
@@ -300,6 +300,7 @@ class DelayedVisitor final : public VNVisitor {
bool m_ignoreBlkAndNBlk = false; // Suppress delayed assignment BLKANDNBLK
bool m_inNonCombLogic = false; // We are in non-combinational logic
bool m_needsInitialTrigger = false; // Whether a NodeProcedure needs a initial trigger
std::vector<AstSenTree*> m_nbaEventSenTreeps; // Sensitivities of '->>' in the process
AstVarRef* m_currNbaLhsRefp = nullptr; // Current NBA LHS variable reference
// STATE - during NBA conversion (after visit)
@@ -1190,6 +1191,7 @@ class DelayedVisitor final : public VNVisitor {
}
void visit(AstNodeProcedure* nodep) override {
VL_RESTORER(m_needsInitialTrigger);
VL_RESTORER_CLEAR(m_nbaEventSenTreeps);
const size_t firstNBAAddedIndex = m_nbas.size();
{
VL_RESTORER(m_inSuspendableOrFork);
@@ -1241,6 +1243,8 @@ class DelayedVisitor final : public VNVisitor {
for (size_t i = firstNBAAddedIndex; i < m_nbas.size(); ++i) {
m_vscpInfo(m_nbas[i].vscp).addSensitivity(senItemp);
}
for (AstSenTree* const senTreep : m_nbaEventSenTreeps)
senTreep->addSensesp(senItemp->cloneTree(true));
// Done with these
VL_DO_DANGLING(senItemp->deleteTree(), senItemp);
}
@@ -1266,8 +1270,15 @@ class DelayedVisitor final : public VNVisitor {
cstmtp->add(");");
AstNode* newp = cstmtp;
if (nodep->isDelayed()) {
const AstVarRef* const vrefp = VN_AS(eventp, VarRef);
const AstVarRef* const vrefp = VN_CAST(eventp, VarRef);
if (nodep->isDelayed() && (m_cfuncp || !vrefp)) {
// V3Timing converts these with --timing
nodep->v3warn(E_NOTIMING, "Nonblocking event trigger "
<< (m_cfuncp ? "in a non-inlined function/task"
: "of a class or interface member or "
"array element")
<< " requires --timing");
} else if (nodep->isDelayed()) {
const std::string newvarname = "__Vdly__" + vrefp->varp()->shortName();
AstVarScope* const dlyvscp
= createTemp(flp, vrefp->varScopep()->scopep(), newvarname, 1);
@@ -1276,20 +1287,35 @@ class DelayedVisitor final : public VNVisitor {
return new AstVarRef{flp, dlyvscp, access};
};
AstAlwaysPre* const prep = new AstAlwaysPre{flp};
prep->addStmtsp(new AstAssign{flp, dlyRef(VAccess::WRITE),
new AstConst{flp, AstConst::BitFalse{}}});
AstAlwaysPost* const postp = new AstAlwaysPost{flp};
{
AstIf* const ifp = new AstIf{flp, dlyRef(VAccess::READ)};
postp->addStmtsp(ifp);
ifp->addThensp(newp);
}
AstIf* const ifp = new AstIf{flp, dlyRef(VAccess::READ)};
postp->addStmtsp(ifp);
UASSERT_OBJ(m_activep, nodep, "No active to handle FireEvent");
AstActive* const activep = new AstActive{flp, "nba-event", m_activep->sentreep()};
AstSenTree* senTreep = m_activep->sentreep();
AstActive* activep = nullptr;
if (m_inSuspendableOrFork && (senTreep->hasInitial() || senTreep->hasClocked())) {
// Suspendable code can trigger whenever it resumes, so fire when its NBAs are
// committed (see visit(AstNodeProcedure*)), clearing the flag like
// Scheme::FlagUnique
senTreep = new AstSenTree{
flp, senTreep->hasClocked() ? senTreep->sensesp()->cloneTree(true) : nullptr};
m_nbaEventSenTreeps.push_back(senTreep);
m_needsInitialTrigger |= m_timingDomains.empty();
dlyvscp->varp()->setIgnorePostWrite();
ifp->addThensp(new AstAssign{flp, dlyRef(VAccess::WRITE),
new AstConst{flp, AstConst::BitFalse{}}});
activep = new AstActive{flp, "nba-event", senTreep};
activep->senTreeStorep(senTreep);
} else {
activep = new AstActive{flp, "nba-event", senTreep};
AstAlwaysPre* const prep = new AstAlwaysPre{flp};
prep->addStmtsp(new AstAssign{flp, dlyRef(VAccess::WRITE),
new AstConst{flp, AstConst::BitFalse{}}});
activep->addStmtsp(prep);
}
ifp->addThensp(newp);
m_activep->addNextHere(activep);
activep->addStmtsp(prep);
activep->addStmtsp(postp);
newp = new AstAssign{flp, dlyRef(VAccess::WRITE),
+52
View File
@@ -389,6 +389,13 @@ class TimingSuspendableVisitor final : public VNVisitor {
if (!VN_IS(m_procp, NodeProcedure)) v3Global.setUsesTiming();
visit(static_cast<AstNode*>(nodep));
}
void visit(AstFireEvent* nodep) override {
if (nodep->isDelayed()
&& (!VN_IS(m_procp, NodeProcedure) || !VN_IS(nodep->operandp(), VarRef))) {
v3Global.setUsesTiming();
}
visit(static_cast<AstNode*>(nodep));
}
void visit(AstAssignW* nodep) override {
if (nodep->timingControlp()) v3Global.setUsesTiming();
// Containing process will not suspend, don't mark it
@@ -1259,6 +1266,51 @@ class TimingControlVisitor final : public VNVisitor {
// Replace the RHS with an intermediate value var
replaceWithIntermediate(nodep->rhsp(), m_intraValueNames.get(nodep));
}
void visit(AstFireEvent* nodep) override {
// V3Delayed handles '->>' of a variable in a process. Like NBAs in non-inlined functions,
// trigger other events from a fork awaiting the NBA region.
if (!nodep->isDelayed() || (m_underProcedure && VN_IS(nodep->operandp(), VarRef))) {
iterateChildren(nodep);
return;
}
FileLine* const flp = nodep->fileline();
AstAssign* const trigAssignp = createNbaEventTriggerAssignment(flp);
nodep->replaceWith(trigAssignp);
AstFork* const forkp = new AstFork{flp, VJoinType::JOIN_NONE};
trigAssignp->addNextHere(forkp);
if (m_underJumpBlock) addCLocalScope(flp, forkp);
// The triggered event is the one referenced now, so evaluate handles and indices now
AstNodeExpr* const eventp = nodep->operandp()->unlinkFrBack();
const auto evalNow = [&](AstNodeExpr* const valuep, V3UniqueNames& names) {
AstVarScope* const vscp = createTemp(flp, names.get(nodep), valuep->dtypep(), forkp);
valuep->replaceWith(new AstVarRef{flp, vscp, VAccess::READ});
// Unlike an event, a handle selecting one is only read
valuep->foreach([](AstNode* const np) {
if (AstNodeVarRef* const refp = VN_CAST(np, NodeVarRef)) {
refp->access(VAccess::READ);
} else if (AstMemberSel* const selp = VN_CAST(np, MemberSel)) {
selp->access(VAccess::READ);
}
});
forkp->addHereThisAsNext(
new AstAssign{flp, new AstVarRef{flp, vscp, VAccess::WRITE}, valuep});
};
AstNodeExpr* refp = eventp;
while (AstNodeSel* const selp = VN_CAST(refp, NodeSel)) {
if (!VN_IS(selp->bitp(), Const)) evalNow(selp->bitp(), m_intraIndexNames);
refp = selp->fromp();
}
// Handle of a class (or virtual interface)
if (AstMemberSel* const selp = VN_CAST(refp, MemberSel)) {
evalNow(selp->fromp(), m_intraValueNames);
}
AstEventControl* const controlp = createNbaEventControl(flp);
controlp->addStmtsp(new AstFireEvent{flp, eventp, false});
AstBegin* const beginp = new AstBegin{flp, "", controlp, false};
addFlags(beginp, T_NBA_UPDATE);
forkp->addForksp(beginp);
VL_DO_DANGLING(pushDeletep(nodep), nodep);
}
void visit(AstAssignW* nodep) override {
FileLine* const flp = nodep->fileline();
// Get the net delay unless this assignment was created for handling the net delay (user1)
@@ -0,0 +1,12 @@
%Error-NOTIMING: t/t_event_nba_notiming_bad.v:10:5: Nonblocking event trigger in a non-inlined function/task requires --timing
: ... note: In instance '$unit::Cls'
10 | ->>e;
| ^~~
... For error description see https://verilator.org/warn/NOTIMING?v=latest
%Error-NOTIMING: t/t_event_nba_notiming_bad.v:20:5: Nonblocking event trigger of a class or interface member or array element requires --timing
20 | ->>c.e;
| ^~~
%Error-NOTIMING: t/t_event_nba_notiming_bad.v:21:5: Nonblocking event trigger of a class or interface member or array element requires --timing
21 | ->>ea[idx];
| ^~~
%Error: Exiting due to
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Nonblocking event triggers requiring --timing
#
# 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('linter')
test.lint(verilator_flags2=["--no-timing"], fails=True, expect_filename=test.golden_filename)
test.passes()
+23
View File
@@ -0,0 +1,23 @@
// DESCRIPTION: Verilator: Nonblocking event triggers requiring --timing
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
class Cls;
event e;
function void trig();
->>e;
endfunction
endclass
module t;
Cls c = new;
event ea[2];
int idx;
initial begin
c.trig();
->>c.e;
->>ea[idx];
end
endmodule
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Nonblocking event triggers in suspendable processes
#
# 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()
+134
View File
@@ -0,0 +1,134 @@
// DESCRIPTION: Verilator: Nonblocking event triggers in suspendable processes
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// verilog_format: off
`define stop $stop
`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d (%s !== %s)\n", `__FILE__,`__LINE__, (gotv), (expv), `"gotv`", `"expv`"); `stop; end while(0);
`define checks(gotv,expv) do if ((gotv) != (expv)) begin $write("%%Error: %s:%0d: got=\"%s\" exp=\"%s\"\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0);
// verilog_format: on
class Cls;
event e;
event ea[2];
static event se;
Cls next;
task trig_dly();
#1 ->>e;
`checkd(e.triggered, 0)
endtask
endclass
interface ifc;
event e;
task trig();
->>e;
endtask
endinterface
module t;
bit clk;
event e_init;
event e_dly;
event e_clk;
event e_arr[2];
int init_time = -1;
int dly_time = -1;
int clk_times;
Cls c1 = new;
Cls c2 = new;
Cls c;
ifc i ();
virtual ifc vif;
int idx;
int x;
string log;
always #5 clk = ~clk;
initial begin
@e_init;
init_time = int'($time);
end
initial begin
@e_dly;
dly_time = int'($time);
end
always @e_clk ++clk_times;
always @(c1.e) log = {log, $sformatf("c1.e@%0d ", $time)};
always @(Cls::se) log = {log, $sformatf("se@%0d ", $time)};
always @(c1.ea[0]) log = {log, $sformatf("c1.ea[0]@%0d ", $time)};
always @(c1.ea[1]) begin
`checkd(x, 3)
log = {log, $sformatf("c1.ea[1]@%0d ", $time)};
end
always @(c2.ea[0]) log = {log, $sformatf("c2.ea[0]@%0d ", $time)};
always @(c2.ea[1]) log = {log, $sformatf("c2.ea[1]@%0d ", $time)};
always @(c2.e) log = {log, $sformatf("c2.e@%0d ", $time)};
always @(e_arr[0]) log = {log, $sformatf("e_arr[0]@%0d ", $time)};
always @(e_arr[1]) log = {log, $sformatf("e_arr[1]@%0d ", $time)};
always @(i.e) log = {log, $sformatf("i.e@%0d ", $time)};
// The event is triggered in the NBA region (IEEE 1800-2023 15.5.1)
initial ->>e_init;
initial begin
#1 ->>e_dly;
`checkd(e_dly.triggered, 0)
#0 `checkd(e_dly.triggered, 0)
end
always begin
@(posedge clk);
->>e_clk;
end
initial begin
vif = i;
// From a suspended class method
c1.trig_dly();
// Static class member
#1 ->>Cls::se;
// The event triggered is the one referenced when '->>' executes
#1 c = c1;
idx = 1;
->>c.ea[idx];
x <= 3;
c = c2;
idx = 0;
#1 idx = 1;
->>e_arr[idx];
idx = 0;
// From an interface task, called by two processes
#1 vif.trig();
// In a loop with a jump
#2 for (int k = 0; k < 2; ++k) begin
if (k == 1) break;
->>c2.ea[k];
end
// Not a subprocess, so not disabled (IEEE 1800-2023 9.6.3)
#1 fork
#1 `stop;
join_none
->>c2.e;
disable fork;
// Including a handle selected from another
#1 c1.next = c2;
->>c1.next.e;
c1.next = null;
end
initial #6 vif.trig();
initial begin
#22;
`checkd(init_time, 0)
`checkd(dly_time, 1)
`checkd(clk_times, 2)
`checks(log, "c1.e@1 se@2 c1.ea[1]@3 e_arr[1]@4 i.e@5 i.e@6 c2.ea[0]@7 c2.e@8 c2.e@9 ")
$write("*-* All Finished *-*\n");
$finish;
end
endmodule