Fix cycle delay in synchronous drive not counting its clocking block (#8487)

This commit is contained in:
Marco Bartoli
2026-10-05 05:49:37 -04:00
committed by GitHub
parent 8cc1ad620e
commit 19254541ee
9 changed files with 532 additions and 24 deletions
+159 -21
View File
@@ -32,6 +32,36 @@
VL_DEFINE_DEBUG_FUNCTIONS;
//######################################################################
// Clocking blocks of clocking items
class AssertPreClockingVisitor final : public VNVisitorConst {
// Captures the clocking block of each clocking item before AssertPreVisitor, which needs it
// at synchronous drives, often visited before the clocking block, e.g. in a parent module
// NODE STATE
// AstClockingItem::user3p() // AstClocking*. Clocking block of the item, user3 claimed by
// // AssertPreVisitor
// VISITORS
void visit(AstNetlist* nodep) override { iterateAndNextConstNull(nodep->modulesp()); }
void visit(AstNodeModule* nodep) override { iterateAndNextConstNull(nodep->stmtsp()); }
void visit(AstGenBlock* nodep) override { iterateAndNextConstNull(nodep->itemsp()); }
void visit(AstClocking* nodep) override {
for (AstNode* itemp = nodep->itemsp(); itemp; itemp = itemp->nextp()) {
if (AstClockingItem* const citemp = VN_CAST(itemp, ClockingItem)) {
citemp->user3p(nodep);
}
}
}
// Clocking blocks are only in modules and their generate blocks
void visit(AstNode*) override {}
public:
// CONSTRUCTORS
explicit AssertPreClockingVisitor(AstNetlist* nodep) { iterateConst(nodep); }
~AssertPreClockingVisitor() override = default;
};
//######################################################################
// Assert class functions
@@ -49,9 +79,12 @@ private:
// NODE STATE
// AstClockingItem::user1p() // AstVar*. varp() of ClockingItem after unlink
// AstClockingItem::user2p() // AstVar*. Flag set by drives of output clockvar
// AstClockingItem::user3p() // AstClocking*. Clocking block of the item, captured
// // by AssertPreClockingVisitor
// AstPExpr::user1() // bool. Created from AstUntil
const VNUser1InUse m_inuser1;
const VNUser2InUse m_inuser2;
const VNUser3InUse m_inuser3;
// STATE
// Current context:
AstNetlist* const m_netlistp = nullptr; // Current netlist
@@ -83,6 +116,7 @@ private:
bool m_inAssign = false; // True if in an AssignNode
bool m_inAssignDlyLhs = false; // True if in AssignDly's LHS
bool m_inSynchDrive = false; // True if in synchronous drive
bool m_inConcatAssign = false; // True if in assignment to a concatenation
std::vector<SynchDrive> m_drives; // Clockvars written by the synchronous drive
bool m_hasCycleDelay = false; // True if node has cycle delay beneath
std::vector<AstVarXRef*> m_xrefsp; // list of xrefs that need name fixup
@@ -100,26 +134,97 @@ private:
}
return VN_AS(itemp->user2p(), Var);
}
// Assignment setting the drive flag of a driven clockvar, referenced like the clockvar
AstAssign* newDrivenSetp(FileLine* flp, const SynchDrive& drive) {
AstVar* const varp = getCreateDrivenVarp(drive.itemp);
AstNodeExpr* refp;
// Reference to a variable in the scope of a driven clockvar, made like the clockvar's
static AstNodeExpr* newDriveRefp(FileLine* flp, const SynchDrive& drive, AstVar* varp,
const VAccess& access) {
if (const AstVarXRef* const xrefp = VN_CAST(drive.refp, VarXRef)) {
refp = new AstVarXRef{flp, varp, xrefp->dotted(), VAccess::WRITE};
return new AstVarXRef{flp, varp, xrefp->dotted(), access};
} else if (const AstMemberSel* const selp = VN_CAST(drive.refp, MemberSel)) {
// The interface expression is evaluated again for the flag, so it must not have side
// effects; cloneTreePure warns if it has any
// The interface expression is evaluated again, so it must not have side effects;
// cloneTreePure warns if it has any
AstMemberSel* const newSelp
= new AstMemberSel{flp, selp->fromp()->cloneTreePure(false), varp};
newSelp->access(VAccess::WRITE);
refp = newSelp;
} else {
refp = new AstVarRef{flp, varp, VAccess::WRITE};
newSelp->access(access);
return newSelp;
}
AstAssign* const setp = new AstAssign{flp, refp, new AstConst{flp, AstConst::BitTrue{}}};
return new AstVarRef{flp, varp, access};
}
// Assignment setting the drive flag of a driven clockvar
AstAssign* newDrivenSetp(FileLine* flp, const SynchDrive& drive) {
AstVar* const varp = getCreateDrivenVarp(drive.itemp);
AstAssign* const setp = new AstAssign{flp, newDriveRefp(flp, drive, varp, VAccess::WRITE),
new AstConst{flp, AstConst::BitTrue{}}};
setp->user1(true);
return setp;
}
// Clocking block of a clocking item
static AstClocking* clockingOf(const AstClockingItem* itemp) {
UASSERT_OBJ(itemp->user3p(), itemp, "Clocking item not captured in a clocking block");
return VN_AS(itemp->user3p(), Clocking);
}
// Clocking event of a driven clockvar, referenced from the drive like the clockvar, or
// nullptr if unsupported, which is reported on the drive's cycle delay
AstSenItem* newDriveSensesp(AstDelay* delayp, const SynchDrive& drive) {
AstSenItem* const origp = clockingOf(drive.itemp)->sensesp();
if (VN_IS(drive.refp, VarRef)) return origp->cloneTree(false);
if (origp->exists([](const AstVarXRef*) { return true; })) {
// The reference is relative to the clocking block
delayp->v3warn(E_UNSUPPORTED, "Unsupported: cycle delay in synchronous drive to"
" clockvar in another scope, whose clocking event"
" has a hierarchical reference");
return nullptr;
}
const AstMemberSel* const selp = VN_CAST(drive.refp, MemberSel);
// Processes outside classes evaluate their events also when not waiting on them,
// possibly before the interface reference is set, so the event is guarded against a
// null reference, which needs an integral value; scheduling does not guard it
const bool guardNull = selp && !VN_IS(m_modp, Class);
if (guardNull && !origp->sensp()->dtypep()->skipRefp()->isIntegralOrPacked()) {
delayp->v3warn(E_UNSUPPORTED, "Unsupported: cycle delay in synchronous drive through"
" an interface reference outside a class, to clocking"
" block with non-integral clocking event");
return nullptr;
}
// Interface declaring the clocking block, if driven through an interface reference
AstIface* const ifacep
= selp ? VN_AS(selp->fromp()->dtypep()->skipRefp(), IfaceRefDType)->ifaceViaCellp()
: nullptr;
AstSenItem* const sensesp = origp->cloneTree(false);
std::vector<AstVarRef*> refps;
sensesp->foreach([&](AstVarRef* refp) {
if (!refp->classOrPackagep()) refps.push_back(refp);
});
for (AstVarRef* const refp : refps) {
AstVar* const varp = refp->varp();
if (ifacep) {
// Mark the variable as read through an interface reference, as V3Width does for
// a member select. The mark is the interface declaring the variable, whose
// instances all share it, so drives through references to any instances agree.
UASSERT_OBJ(!varp->sensIfacep() || varp->sensIfacep() == ifacep, refp,
"Variable read through references to different interfaces");
varp->sensIfacep(ifacep);
}
refp->replaceWith(newDriveRefp(refp->fileline(), drive, varp, refp->access()));
VL_DO_DANGLING(pushDeletep(refp), refp);
}
if (guardNull) guardNullIface(sensesp, selp->fromp());
return sensesp;
}
// Make an integral clocking event through an interface reference false while the reference
// is null
static void guardNullIface(AstSenItem* sensesp, AstNodeExpr* fromp) {
FileLine* const flp = sensesp->fileline();
AstNodeExpr* const sensp = sensesp->sensp()->unlinkFrBack();
sensesp->sensp(new AstCond{flp, newNotNullp(flp, fromp), sensp,
new AstConst{flp, AstConst::DTyped{}, sensp->dtypep()}});
if (AstNodeExpr* const condp = sensesp->condp()) {
condp->unlinkFrBack();
sensesp->condp(new AstLogAnd{flp, newNotNullp(flp, fromp), condp});
}
}
static AstNodeExpr* newNotNullp(FileLine* flp, AstNodeExpr* fromp) {
return new AstNeq{flp, fromp->cloneTreePure(false), new AstConst{flp, AstConst::Null{}}};
}
static void checkSamplingFuncDType(AstNodeExpr* nodep, const AstNode* exprp) {
const AstNodeDType* const dtypep = exprp->dtypep()->skipRefp();
@@ -493,12 +598,12 @@ private:
FileLine* const flp = nodep->fileline();
AstNodeExpr* valuep = V3Const::constifyEdit(nodep->lhsp()->unlinkFrBack());
const AstConst* const constp = VN_CAST(valuep, Const);
if (!constp) {
if (!constp && !m_inSynchDrive) {
// V3AssertNfa handles non-const delays before this pass and
// replaces the property; this branch should never be reached.
nodep->v3fatalSrc("Non-constant cycle delay in assertion: "
"should have been caught by V3AssertNfa");
} else if (constp->isZero()) {
} else if (constp && constp->isZero()) {
VL_DO_DANGLING(pushDeletep(valuep), valuep);
if (m_inSynchDrive) {
// ##0 has no effect in synchronous drives (IEEE 1800-2023 14.11)
@@ -535,20 +640,36 @@ private:
return;
}
AstSenItem* sensesp = nullptr;
if (!m_defaultClockingp) {
if (!m_drives.empty()) {
if (m_inConcatAssign) {
// Also the clockvars may be of different clocking blocks
nodep->v3error("Cycle delays cannot be used in synchronous drives to a"
" concatenation (IEEE 1800-2023 14.16)");
VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep);
VL_DO_DANGLING(pushDeletep(valuep), valuep);
return;
}
// Count the cycles of the driven clockvar's clocking block (IEEE 1800-2023 14.16)
sensesp = newDriveSensesp(nodep, m_drives.front());
if (!sensesp) {
VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep);
VL_DO_DANGLING(pushDeletep(valuep), valuep);
return;
}
} else if (!m_defaultClockingp) {
if (!m_pexprp) {
nodep->v3error("Usage of cycle delays requires default clocking"
" (IEEE 1800-2023 14.11)");
VL_DO_DANGLING(nodep->unlinkFrBack()->deleteTree(), nodep);
VL_DO_DANGLING(valuep->deleteTree(), valuep);
VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep);
VL_DO_DANGLING(pushDeletep(valuep), valuep);
return;
}
sensesp = m_senip;
sensesp = m_senip->cloneTree(false);
} else {
sensesp = m_defaultClockingp->sensesp();
sensesp = m_defaultClockingp->sensesp()->cloneTree(false);
}
AstEventControl* const controlp = new AstEventControl{
nodep->fileline(), new AstSenTree{flp, sensesp->cloneTree(false)}, nullptr};
AstEventControl* const controlp
= new AstEventControl{nodep->fileline(), new AstSenTree{flp, sensesp}, nullptr};
const std::string delayName = m_cycleDlyNames.get(nodep);
AstNodeExpr* throughoutp
= nodep->throughoutp() ? nodep->throughoutp()->unlinkFrBack() : nullptr;
@@ -557,6 +678,15 @@ private:
nodep->findBasicDType(VBasicDTypeKwd::UINT32)};
cntVarp->lifetime(VLifetime::AUTOMATIC_EXPLICIT);
AstBegin* const beginp = new AstBegin{flp, delayName + "__block", cntVarp, true};
// A non-constant delay of a synchronous drive is counted at runtime, waiting for none if
// zero, like a constant '##0' in a synchronous drive. The count is integral of 64 bits,
// as V3Width makes delays, or real, which is rounded (IEEE 1800-2023 6.12.1)
if (valuep->isDouble()) {
valuep = new AstRToIRoundS{flp, valuep};
valuep->dtypeFrom(cntVarp);
} else {
valuep = new AstSel{flp, valuep, 0, cntVarp->width()};
}
beginp->addStmtsp(new AstAssign{flp, new AstVarRef{flp, cntVarp, VAccess::WRITE}, valuep});
// Throughout: create flag tracking whether condition held every tick
@@ -703,9 +833,16 @@ private:
if (nodep->user1()) return;
VL_RESTORER(m_inAssign);
VL_RESTORER(m_inSynchDrive);
VL_RESTORER(m_inConcatAssign);
VL_RESTORER_CLEAR(m_drives);
m_inAssign = true;
m_inSynchDrive = false;
{
// The braces of a concatenation are a replication, by one
const AstNodeExpr* lhsp = nodep->lhsp();
if (const AstReplicate* const repp = VN_CAST(lhsp, Replicate)) lhsp = repp->srcp();
m_inConcatAssign = VN_IS(lhsp, Concat);
}
{
VL_RESTORER(m_inAssignDlyLhs);
m_inAssignDlyLhs = VN_IS(nodep, AssignDly);
@@ -1686,6 +1823,7 @@ public:
// CONSTRUCTORS
explicit AssertPreVisitor(AstNetlist* nodep)
: m_netlistp{nodep} {
{ AssertPreClockingVisitor{nodep}; }
// Process
iterate(nodep);
// Fix up varref names
+4
View File
@@ -43,4 +43,8 @@
: ... note: In instance 't'
39 | $display(cb1.out);
| ^~~
%Error: t/t_clocking_bad4.v:46:33: Cycle delays cannot be used in synchronous drives to a concatenation (IEEE 1800-2023 14.16)
: ... note: In instance 't'
46 | always {cb1.out, cb3.out3} <= ##1 2'b11;
| ^~
%Error: Exiting due to
+6
View File
@@ -38,4 +38,10 @@ module t (
cb1.in = 1;
$display(cb1.out);
end
logic out3;
clocking cb3 @(posedge clk);
output out3;
endclocking
always {cb1.out, cb3.out3} <= ##1 2'b11;
endmodule
+9 -3
View File
@@ -35,12 +35,18 @@ module t;
b = 0;
@(pe);
{pe.a, pe.b} <= 2'b11;
#2;
a = 0;
b = 0;
// '##0' has no effect, so is accepted, unlike other cycle delays (see t_clocking_bad4)
@(pe);
{pe.a, pe.b} <= ##0 2'b11;
end
initial begin
#30;
`checks(a_log, "1@5 0@7 1@15 ")
`checks(b_log, "1@5 0@7 1@15 ")
#40;
`checks(a_log, "1@5 0@7 1@15 0@17 1@25 ")
`checks(b_log, "1@5 0@7 1@15 0@17 1@25 ")
$write("*-* All Finished *-*\n");
$finish;
end
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Cycle delays of synchronous drives count the target clocking block
#
# 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()
+276
View File
@@ -0,0 +1,276 @@
// DESCRIPTION: Verilator: Cycle delays of synchronous drives count the target clocking block
//
// 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 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
package pkg;
bit pclk;
endpackage
interface bus_if (
input bit clk
);
bit w;
bit ws;
bit z;
clocking cb @(posedge clk);
output w, z;
output #1 ws;
endclocking
modport tb(clocking cb);
endinterface
interface en_if (
input bit clk
);
bit en;
bit y;
clocking icb @(posedge clk iff en);
output y;
endclocking
endinterface
interface ev_if;
event ev;
bit e;
clocking ecb @(ev);
output e;
endclocking
endinterface
// A class has no default clocking
class Driver;
virtual bus_if vif;
virtual bus_if.tb tvif;
virtual bus_if vifs[2];
virtual ev_if vvif;
task run();
@(vif.cb);
vif.cb.w <= ##2 1;
tvif.cb.w <= ##2 1;
tvif.cb.ws <= ##2 1;
vifs[0].cb.w <= ##2 1;
vvif.ecb.e <= ##2 1;
vif.cb.z <= ##0 1;
endtask
endclass
// No default clocking is needed for a cycle delay in a drive
module sub (
input bit clk,
output bit [1:0] q
);
bit [6:0] s;
bit n;
bit p;
clocking cb @(posedge clk);
output q, s;
endclocking
clocking ncb @(negedge clk);
output n;
endclocking
clocking pcb @(posedge pkg::pclk);
output p;
endclocking
for (genvar i = 0; i < 2; ++i) begin : g
bit x;
clocking cb @(posedge clk);
output x;
endclocking
end
initial begin
@(cb);
cb.q <= ##1 2;
end
endmodule
module port_drv (
bus_if b
);
initial begin
@(b.cb);
b.cb.w <= ##2 1;
end
endmodule
module t;
bit clk;
bit tclk;
bit slow_clk;
bit [1:0] v;
bit [1:0] q;
bit c;
bit c0;
bit c7;
bit cr;
bit cr5;
bit d;
int cycles = 2;
int nocycles = 0;
bit [6:0] cycles7 = 2;
real rcycles = 1.6;
string v_log;
string q_log;
string s_log;
string n_log;
string p_log;
string x_log;
string x0_log;
string c_log;
string c0_log;
string c7_log;
string cr_log;
string cr5_log;
string proc_time;
string d_log;
string z_log;
string w_log;
string tw_log;
string tws_log;
string w0_log;
string w1_log;
string mw_log;
string pw_log;
string y_log;
string e_log;
Driver drv = new;
always #5 clk = ~clk;
always #7 tclk = ~tclk;
always #20 slow_clk = ~slow_clk;
always @(clk) pkg::pclk = clk;
default clocking slow @(posedge slow_clk);
output d;
endclocking
clocking fast @(posedge clk);
output v, c, c0, c7, cr, cr5;
endclocking
sub sub (
.clk,
.q
);
bus_if bus (.clk);
// Another instance of the interface, with another clock
bus_if tbus (.clk(tclk));
bus_if buses[2] (.clk);
bus_if mbus (.clk);
bus_if pbus (.clk);
en_if ebus (.clk);
ev_if vbus ();
port_drv port_drv (.b(pbus));
virtual bus_if mvif;
virtual en_if evif;
initial forever #10->vbus.ev;
always @(v) if ($time != 0) v_log = {v_log, $sformatf("%0d@%0d ", v, $time)};
always @(q) if ($time != 0) q_log = {q_log, $sformatf("%0d@%0d ", q, $time)};
always @(sub.s) if ($time != 0) s_log = {s_log, $sformatf("%0d@%0d ", sub.s, $time)};
always @(sub.n) if ($time != 0) n_log = {n_log, $sformatf("%0d@%0d ", sub.n, $time)};
always @(sub.p) if ($time != 0) p_log = {p_log, $sformatf("%0d@%0d ", sub.p, $time)};
always @(sub.g[1].x) if ($time != 0) x_log = {x_log, $sformatf("%0d@%0d ", sub.g[1].x, $time)};
always @(sub.g[0].x) if ($time != 0) x0_log = {x0_log, $sformatf("%0d@%0d ", sub.g[0].x, $time)};
always @(c) if ($time != 0) c_log = {c_log, $sformatf("%0d@%0d ", c, $time)};
always @(c0) if ($time != 0) c0_log = {c0_log, $sformatf("%0d@%0d ", c0, $time)};
always @(c7) if ($time != 0) c7_log = {c7_log, $sformatf("%0d@%0d ", c7, $time)};
always @(cr) if ($time != 0) cr_log = {cr_log, $sformatf("%0d@%0d ", cr, $time)};
always @(cr5) if ($time != 0) cr5_log = {cr5_log, $sformatf("%0d@%0d ", cr5, $time)};
always @(d) if ($time != 0) d_log = {d_log, $sformatf("%0d@%0d ", d, $time)};
always @(bus.z) if ($time != 0) z_log = {z_log, $sformatf("%0d@%0d ", bus.z, $time)};
always @(bus.w) if ($time != 0) w_log = {w_log, $sformatf("%0d@%0d ", bus.w, $time)};
always @(tbus.w) if ($time != 0) tw_log = {tw_log, $sformatf("%0d@%0d ", tbus.w, $time)};
always @(tbus.ws) if ($time != 0) tws_log = {tws_log, $sformatf("%0d@%0d ", tbus.ws, $time)};
always @(buses[0].w) if ($time != 0) w0_log = {w0_log, $sformatf("%0d@%0d ", buses[0].w, $time)};
always @(buses[1].w) if ($time != 0) w1_log = {w1_log, $sformatf("%0d@%0d ", buses[1].w, $time)};
always @(mbus.w) if ($time != 0) mw_log = {mw_log, $sformatf("%0d@%0d ", mbus.w, $time)};
always @(pbus.w) if ($time != 0) pw_log = {pw_log, $sformatf("%0d@%0d ", pbus.w, $time)};
always @(ebus.y) if ($time != 0) y_log = {y_log, $sformatf("%0d@%0d ", ebus.y, $time)};
always @(vbus.e) if ($time != 0) e_log = {e_log, $sformatf("%0d@%0d ", vbus.e, $time)};
initial begin
mvif = mbus;
evif = ebus;
drv.vif = bus;
drv.tvif = tbus;
drv.vifs[0] = buses[0];
drv.vifs[1] = buses[1];
drv.vvif = vbus;
drv.run();
end
// Each drive counts the cycles of the target clockvar's clocking block, not of the default
// clocking (IEEE 1800-2023 14.16)
initial begin
@(fast);
fast.v <= ##2 1;
sub.cb.s <= ##2 7'h55;
sub.ncb.n <= ##1 1;
sub.pcb.p <= ##1 1;
sub.g[1].cb.x <= ##2 1;
// The default clocking counts its cycles as before
slow.d <= ##1 1;
// A non-constant cycle delay is evaluated by the drive, and counted at runtime
fast.c <= ##cycles 1;
fast.c0 <= ##nocycles 1;
fast.c7 <= ##cycles7 1;
// A real cycle count is rounded (IEEE 1800-2023 6.12.1)
fast.cr <= ##rcycles 1;
fast.cr5 <= ##(2.5) 1;
sub.g[0].cb.x <= ##cycles 1;
buses[1].cb.w <= ##2 1;
mvif.cb.w <= ##2 1;
end
// The clocking event of icb only occurs while en is set
initial begin
#1 evif.icb.y <= ##1 1;
#20 ebus.en = 1;
end
// Also in a procedural cycle delay, of the default clocking
initial begin
#2;
##(1.5);
proc_time = $sformatf("%0d", $time);
end
initial begin
#100;
`checks(v_log, "1@25 ")
`checks(q_log, "2@15 ")
`checks(s_log, "85@25 ")
`checks(n_log, "1@10 ")
`checks(p_log, "1@15 ")
`checks(x_log, "1@25 ")
`checks(x0_log, "1@25 ")
`checks(c_log, "1@25 ")
`checks(c0_log, "1@5 ")
`checks(c7_log, "1@25 ")
`checks(cr_log, "1@25 ")
`checks(cr5_log, "1@35 ")
`checks(d_log, "1@20 ")
`checks(proc_time, "60")
`checks(z_log, "1@5 ")
`checks(w_log, "1@25 ")
`checks(tw_log, "1@21 ")
`checks(tws_log, "1@22 ")
`checks(w0_log, "1@25 ")
`checks(w1_log, "1@25 ")
`checks(mw_log, "1@25 ")
`checks(pw_log, "1@25 ")
`checks(y_log, "1@25 ")
`checks(e_log, "1@20 ")
$write("*-* All Finished *-*\n");
$finish;
end
endmodule
@@ -0,0 +1,10 @@
%Error-UNSUPPORTED: t/t_clocking_drive_cycle_unsup.v:18:24: Unsupported: cycle delay in synchronous drive to clockvar in another scope, whose clocking event has a hierarchical reference
: ... note: In instance 't'
18 | initial sub.scb.s <= ##1 1;
| ^~
... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest
%Error-UNSUPPORTED: t/t_clocking_drive_cycle_unsup.v:24:19: Unsupported: cycle delay in synchronous drive through an interface reference outside a class, to clocking block with non-integral clocking event
: ... note: In instance 't'
24 | vvif.ecb.e <= ##1 1;
| ^~
%Error: Exiting due to
+16
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: 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()
@@ -0,0 +1,34 @@
// 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
module sub;
bit s;
clocking scb @(posedge t.clk);
output s;
endclocking
initial scb.s <= ##1 1;
endmodule
module t;
bit clk;
sub sub ();
initial sub.scb.s <= ##1 1;
ev_if vbus ();
virtual ev_if vvif;
initial begin
vvif = vbus;
vvif.ecb.e <= ##1 1;
end
endmodule
interface ev_if;
event ev;
bit e;
clocking ecb @(ev);
output e;
endclocking
endinterface