diff --git a/src/V3Delayed.cpp b/src/V3Delayed.cpp index a268e3ccb..5b75e1111 100644 --- a/src/V3Delayed.cpp +++ b/src/V3Delayed.cpp @@ -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 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), diff --git a/src/V3Timing.cpp b/src/V3Timing.cpp index cfc015dd4..9b1accf11 100644 --- a/src/V3Timing.cpp +++ b/src/V3Timing.cpp @@ -389,6 +389,13 @@ class TimingSuspendableVisitor final : public VNVisitor { if (!VN_IS(m_procp, NodeProcedure)) v3Global.setUsesTiming(); visit(static_cast(nodep)); } + void visit(AstFireEvent* nodep) override { + if (nodep->isDelayed() + && (!VN_IS(m_procp, NodeProcedure) || !VN_IS(nodep->operandp(), VarRef))) { + v3Global.setUsesTiming(); + } + visit(static_cast(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) diff --git a/test_regress/t/t_event_nba_notiming_bad.out b/test_regress/t/t_event_nba_notiming_bad.out new file mode 100644 index 000000000..7c6eaa917 --- /dev/null +++ b/test_regress/t/t_event_nba_notiming_bad.out @@ -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 diff --git a/test_regress/t/t_event_nba_notiming_bad.py b/test_regress/t/t_event_nba_notiming_bad.py new file mode 100755 index 000000000..cade2831e --- /dev/null +++ b/test_regress/t/t_event_nba_notiming_bad.py @@ -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() diff --git a/test_regress/t/t_event_nba_notiming_bad.v b/test_regress/t/t_event_nba_notiming_bad.v new file mode 100644 index 000000000..5ac8786e1 --- /dev/null +++ b/test_regress/t/t_event_nba_notiming_bad.v @@ -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 diff --git a/test_regress/t/t_event_nba_timing.py b/test_regress/t/t_event_nba_timing.py new file mode 100755 index 000000000..5c33579db --- /dev/null +++ b/test_regress/t/t_event_nba_timing.py @@ -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() diff --git a/test_regress/t/t_event_nba_timing.v b/test_regress/t/t_event_nba_timing.v new file mode 100644 index 000000000..1d2bc6b0d --- /dev/null +++ b/test_regress/t/t_event_nba_timing.v @@ -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