Support NBAs in initial blocks (#7754)

This commit is contained in:
Igor Zaworski 2026-06-20 23:23:05 +02:00 committed by GitHub
parent 047d6e03d9
commit e269b914b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 207 additions and 55 deletions

View File

@ -0,0 +1,7 @@
.. comment: generated by t_finaldly_bad
.. code-block:: sv
:linenos:
:emphasize-lines: 2
bit foo;
final foo <= 1; // <--- Error

View File

@ -0,0 +1,6 @@
.. comment: generated by t_finaldly_bad
.. code-block::
%Error-FINALDLY: example.v:1:13 Non-blocking assignment '<=' in final block
9 | final foo <= 1;
| ^~

View File

@ -883,6 +883,22 @@ List Of Warnings
with a newline."
.. option:: FINALDLY
Error issued when a non-blocking assignment `<=` is used in a
`final` block.
This error can be disabled. If disabled, the assignment will be
executed as a `=` blocking assignment.
Faulty example:
.. include:: ../../docs/gen/ex_FINALDLY_faulty.rst
Results in:
.. include:: ../../docs/gen/ex_FINALDLY_msg.rst
.. option:: FSMMULTI
Warns that the same always block contains multiple enum-typed case
@ -1179,7 +1195,7 @@ List Of Warnings
.. option:: INITIALDLY
.. TODO better example
Historical, never issued since version 5.050.
Warns that the code has a delayed assignment inside of an ``initial`` or
``final`` block. If this message is suppressed, Verilator will convert

View File

@ -351,7 +351,7 @@ public:
class ActiveDlyVisitor final : public VNVisitor {
public:
enum CheckType : uint8_t { CT_SEQ, CT_COMB, CT_INITIAL, CT_SUSPENDABLE };
enum CheckType : uint8_t { CT_COMB, CT_FINAL };
private:
// MEMBERS
@ -359,15 +359,9 @@ private:
// VISITORS
void visit(AstAssignDly* nodep) override {
// Non-blocking assignments are OK in sequential processes
if (m_check == CT_SEQ || m_check == CT_SUSPENDABLE) return;
// Issue appropriate warning
if (m_check == CT_INITIAL) {
nodep->v3warn(INITIALDLY,
"Non-blocking assignment '<=' in initial/final block\n"
<< nodep->warnMore()
<< "... This will be executed as a blocking assignment '='!");
if (m_check == CT_FINAL) {
nodep->v3warn(FINALDLY, "Non-blocking assignment '<=' in final block");
} else {
nodep->v3warn(COMBDLY,
"Non-blocking assignment '<=' in combinational logic process\n"
@ -465,11 +459,7 @@ class ActiveVisitor final : public VNVisitor {
wantactivep->addStmtsp(nodep);
// Warn and convert any delayed assignments
{
ActiveDlyVisitor{nodep, !m_clockedProcess ? ActiveDlyVisitor::CT_COMB
: oldsentreep ? ActiveDlyVisitor::CT_SEQ
: ActiveDlyVisitor::CT_SUSPENDABLE};
}
if (!m_clockedProcess) ActiveDlyVisitor{nodep, ActiveDlyVisitor::CT_COMB};
// Delete sensitivity list
if (oldsentreep) VL_DO_DANGLING(oldsentreep->deleteTree(), oldsentreep);
@ -509,17 +499,11 @@ class ActiveVisitor final : public VNVisitor {
void visit(AstInitialStatic* nodep) override { moveUnderSpecial<AstSenItem::Static>(nodep); }
void visit(AstInitial* nodep) override {
const bool timedInitial
= v3Global.opt.timing().isSetTrue() && nodep->exists([](const AstNode* const subp) {
return VN_IS(subp, Delay) || VN_IS(subp, EventControl);
});
const ActiveDlyVisitor dlyvisitor{nodep, timedInitial ? ActiveDlyVisitor::CT_SUSPENDABLE
: ActiveDlyVisitor::CT_INITIAL};
visitSenItems(nodep);
moveUnderSpecial<AstSenItem::Initial>(nodep);
}
void visit(AstFinal* nodep) override {
const ActiveDlyVisitor dlyvisitor{nodep, ActiveDlyVisitor::CT_INITIAL};
const ActiveDlyVisitor dlyvisitor{nodep, ActiveDlyVisitor::CT_FINAL};
moveUnderSpecial<AstSenItem::Final>(nodep);
}
void visit(AstCoverToggle* nodep) override { moveUnderSpecial<AstSenItem::Combo>(nodep); }

View File

@ -1428,6 +1428,7 @@ public:
ET_EVENT, // VlEventBase::isFired
// Involving an expression
ET_TRUE,
ET_INITIAL_NBA, // Event that is fired initially and never again
//
ET_COMBO, // Sensitive to all combo inputs to this block
ET_COMBO_STAR, // Sensitive to all combo inputs to this block (from .*)
@ -1446,6 +1447,7 @@ public:
true, // ET_NEGEDGE
true, // ET_EVENT
true, // ET_TRUE
true, // ET_INITIAL_NBA
false, // ET_COMBO
false, // ET_COMBO_STAR
@ -1469,14 +1471,14 @@ public:
}
const char* ascii() const {
static const char* const names[]
= {"CHANGED", "BOTH", "POS", "NEG", "EVENT", "TRUE", "COMBO",
"COMBO_STAR", "HYBRID", "STATIC", "INITIAL", "FINAL", "NEVER"};
= {"CHANGED", "BOTH", "POS", "NEG", "EVENT", "TRUE", "ET_INITIAL_NBA",
"COMBO", "COMBO_STAR", "HYBRID", "STATIC", "INITIAL", "FINAL", "NEVER"};
return names[m_e];
}
const char* verilogKwd() const {
static const char* const names[]
= {"[changed]", "edge", "posedge", "negedge", "[event]", "[true]", "*",
"*", "[hybrid]", "[static]", "[initial]", "[final]", "[never]"};
static const char* const names[] = {
"[changed]", "edge", "posedge", "negedge", "[event]", "[true]", "[initial_nba]",
"*", "*", "[hybrid]", "[static]", "[initial]", "[final]", "[never]"};
return names[m_e];
}
// Return true iff this and the other have mutually exclusive transitions

View File

@ -1753,6 +1753,7 @@ public:
class Combo {}; // for constructor type-overload selection
class Static {}; // for constructor type-overload selection
class Initial {}; // for constructor type-overload selection
class InitialNBA {}; // for constructor type-overload selection
class Final {}; // for constructor type-overload selection
class Never {}; // for constructor type-overload selection
AstSenItem(FileLine* fl, VEdgeType edgeType, AstNodeExpr* senp, AstNodeExpr* condp = nullptr)
@ -1770,6 +1771,9 @@ public:
AstSenItem(FileLine* fl, Initial)
: ASTGEN_SUPER_SenItem(fl)
, m_edgeType{VEdgeType::ET_INITIAL} {}
AstSenItem(FileLine* fl, InitialNBA)
: ASTGEN_SUPER_SenItem(fl)
, m_edgeType{VEdgeType::ET_INITIAL_NBA} {}
AstSenItem(FileLine* fl, Final)
: ASTGEN_SUPER_SenItem(fl)
, m_edgeType{VEdgeType::ET_FINAL} {}

View File

@ -275,6 +275,7 @@ class DelayedVisitor final : public VNVisitor {
bool m_inSuspendableOrFork = false; // True in suspendable processes and forks
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
AstVarRef* m_currNbaLhsRefp = nullptr; // Current NBA LHS variable reference
// STATE - during NBA conversion (after visit)
@ -291,6 +292,7 @@ class DelayedVisitor final : public VNVisitor {
VDouble0 m_nSchemeValueQueuesWhole; // Number of variables using Scheme::ValueQueueWhole
VDouble0 m_nSchemeValueQueuesPartial; // Number of variables using Scheme::ValueQueuePartial
VDouble0 m_nSharedSetFlags; // "Set" flags actually shared by Scheme::FlagShared variables
VDouble0 m_nInitialNBA; // Number of procedural blocks with initial NBA
// METHODS
@ -999,6 +1001,12 @@ class DelayedVisitor final : public VNVisitor {
m_writeRefs(nodep->varScopep()).emplace_back(nodep, nonBlocking, m_inNonCombLogic);
}
template <typename Procedure_T>
static bool isProcedureWithSentreep(const AstNodeProcedure* const nodep) {
const Procedure_T* const procedurep = AstNode::cast<Procedure_T>(nodep);
return procedurep && procedurep->sentreep();
}
// VISITORS
void visit(AstNetlist* nodep) override {
iterateChildren(nodep);
@ -1112,21 +1120,36 @@ class DelayedVisitor final : public VNVisitor {
iterateChildren(nodep);
}
void visit(AstNodeProcedure* nodep) override {
VL_RESTORER(m_needsInitialTrigger);
const size_t firstNBAAddedIndex = m_nbas.size();
{
VL_RESTORER(m_inSuspendableOrFork);
VL_RESTORER(m_procp);
VL_RESTORER(m_ignoreBlkAndNBlk);
VL_RESTORER(m_inNonCombLogic);
m_inSuspendableOrFork = nodep->isSuspendable();
// When we are dealing with initial block we need to
// treat it as suspendable when we meet a NBA
m_inSuspendableOrFork = nodep->isSuspendable() || VN_IS(nodep, Initial);
m_procp = nodep;
if (m_inSuspendableOrFork) {
if (nodep->isSuspendable()) {
m_ignoreBlkAndNBlk = false;
m_inNonCombLogic = true;
}
iterateChildren(nodep);
}
if (m_timingDomains.empty()) return;
auto containsClocled = [](const AstSenItem* itemp) {
while (itemp) {
if (itemp->edgeType().clockedStmt()) return true;
itemp = VN_AS(itemp->nextp(), SenItem);
}
return false;
};
const bool addInitialTrigger = m_needsInitialTrigger
&& !(isProcedureWithSentreep<AstAlways>(nodep)
|| isProcedureWithSentreep<AstAlwaysObserved>(nodep)
|| isProcedureWithSentreep<AstAlwaysReactive>(nodep))
&& !containsClocled(m_activep->sentreep()->sensesp());
if (m_timingDomains.empty() && !addInitialTrigger) return;
// There were some timing domains involved in the process. Add all of them as sensitivities
// of all NBA targets in this process. Note this is a bit of a sledgehammer, we should only
@ -1135,6 +1158,11 @@ class DelayedVisitor final : public VNVisitor {
// First gather all senItems
AstSenItem* senItemp = nullptr;
if (addInitialTrigger) {
senItemp = new AstSenItem{nodep->fileline(), AstSenItem::InitialNBA{}};
++m_nInitialNBA;
}
for (const AstSenTree* const domainp : m_timingDomains) {
if (domainp->sensesp())
senItemp = AstNode::addNext(senItemp, domainp->sensesp()->cloneTree(true));
@ -1218,6 +1246,8 @@ class DelayedVisitor final : public VNVisitor {
UASSERT_OBJ(m_inSuspendableOrFork || m_activep->hasClocked(), nodep,
"<= assignment in non-clocked block, should have been converted in V3Active");
m_needsInitialTrigger |= m_timingDomains.empty();
// Record scope of this NBA
nodep->user2p(m_scopep);
@ -1327,6 +1357,7 @@ public:
V3Stats::addStat("NBA, variables using ValueQueuePartial scheme",
m_nSchemeValueQueuesPartial);
V3Stats::addStat("Optimizations, NBA flags shared", m_nSharedSetFlags);
V3Stats::addStat("Procedures needing initial NBA trigger", m_nInitialNBA);
}
};

View File

@ -107,6 +107,7 @@ public:
ENUMITEMWIDTH, // Error: enum item width mismatch
ENUMVALUE, // Error: enum type needs explicit cast
EOFNEWLINE, // End-of-file missing newline
FINALDLY, // Final delayed statement
FSMMULTI, // Multiple FSM candidates in one always block
FUNCTIMECTL, // Functions cannot have timing/delay/wait
FUTURE, // Feature is under development and not yet supported
@ -227,8 +228,8 @@ public:
"BSSPACE", "CASEINCOMPLETE", "CASEOVERLAP", "CASEWITHX", "CASEX", "CASTCONST",
"CDCRSTLOGIC", "CLKDATA", "CMPCONST", "COLONPLUS", "COMBDLY", "CONSTRAINTIGN",
"CONTASSREG", "COVERIGN", "DECLFILENAME", "DEFOVERRIDE", "DEFPARAM", "DEPRECATED",
"ENCAPSULATED", "ENDLABEL", "ENUMITEMWIDTH", "ENUMVALUE", "EOFNEWLINE", "FSMMULTI",
"FUNCTIMECTL", "FUTURE", "GENCLK", "GENUNNAMED", "HIERBLOCK", "HIERPARAM",
"ENCAPSULATED", "ENDLABEL", "ENUMITEMWIDTH", "ENUMVALUE", "EOFNEWLINE", "FINALDLY",
"FSMMULTI", "FUNCTIMECTL", "FUTURE", "GENCLK", "GENUNNAMED", "HIERBLOCK", "HIERPARAM",
"IEEEMAYDEPRECATE", "IFDEPTH", "IGNOREDRETURN", "IMPERFECTSCH", "IMPLICIT",
"IMPLICITSTATIC", "IMPORTSTAR", "IMPURE", "INCABSPATH", "INFINITELOOP", "INITIALDLY",
"INSECURE", "INSIDETRUE", "LATCH", "LITENDIAN", "MINTYPMAXDLY", "MISINDENT", "MODDUP",
@ -269,11 +270,11 @@ public:
bool pretendError() const VL_MT_SAFE {
return (m_e == ASSIGNIN || m_e == BADSTDPRAGMA || m_e == BADVLTPRAGMA || m_e == BLKANDNBLK
|| m_e == BLKLOOPINIT || m_e == CONTASSREG || m_e == ENCAPSULATED
|| m_e == ENDLABEL || m_e == ENUMITEMWIDTH || m_e == ENUMVALUE || m_e == HIERPARAM
|| m_e == FUNCTIMECTL || m_e == IMPURE || m_e == MODMISSING || m_e == NOTREDOP
|| m_e == PARAMNODEFAULT || m_e == PINNOTFOUND || m_e == PKGNODECL
|| m_e == PROCASSWIRE || m_e == PROTOTYPEMIS || m_e == SUPERNFIRST
|| m_e == ZEROREPL);
|| m_e == ENDLABEL || m_e == ENUMITEMWIDTH || m_e == ENUMVALUE || m_e == FINALDLY
|| m_e == HIERPARAM || m_e == FUNCTIMECTL || m_e == IMPURE || m_e == MODMISSING
|| m_e == NOTREDOP || m_e == PARAMNODEFAULT || m_e == PINNOTFOUND
|| m_e == PKGNODECL || m_e == PROCASSWIRE || m_e == PROTOTYPEMIS
|| m_e == SUPERNFIRST || m_e == ZEROREPL);
}
// Warnings to mention manual
bool mentionManual() const VL_MT_SAFE {

View File

@ -78,7 +78,7 @@ private:
// Check if expression contains a class member access that could be null
// (e.g., accessing an event through a class reference that may not be initialized)
static bool hasClassMemberAccess(const AstNode* const exprp) {
return exprp->exists([](const AstNode* const nodep) {
return exprp && exprp->exists([](const AstNode* const nodep) {
if (const AstMemberSel* const mselp = VN_CAST(nodep, MemberSel)) {
// Check if the base expression is a class reference
return mselp->fromp()->dtypep()
@ -294,6 +294,8 @@ private:
}
case VEdgeType::ET_TRUE: //
return {currp(), false};
case VEdgeType::ET_INITIAL_NBA: //
return {new AstConst{flp, AstConst::BitFalse{}}, true};
default: // LCOV_EXCL_START
senItemp->v3fatalSrc("Unknown edge type");
return {nullptr, false};

View File

@ -49,7 +49,7 @@
//See also: https://github.com/twosigma/verilator_support
// verilog_format: off
// verilator lint_off COMBDLY,INITIALDLY,LATCH,MULTIDRIVEN,UNSIGNED,WIDTH
// verilator lint_off COMBDLY,LATCH,MULTIDRIVEN,UNSIGNED,WIDTH
// BEGINNING OF MODULE
`timescale 1 ps / 1 ps

View File

@ -0,0 +1,5 @@
%Error-FINALDLY: t/t_finaldly_bad.v:9:13: Non-blocking assignment '<=' in final block
9 | final foo <= 1;
| ^~
... For error description see https://verilator.org/warn/FINALDLY?v=latest
%Error: Exiting due to

View File

@ -4,14 +4,21 @@
# 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('linter')
test.top_filename = "t/t_initial_dlyass.v"
test.lint(fails=True, expect_filename=test.golden_filename)
test.compile(fails=True, expect_filename=test.golden_filename)
test.extract(in_filename=test.top_filename,
out_filename=test.root + "/docs/gen/ex_FINALDLY_faulty.rst",
lines="8-9")
test.extract(in_filename=test.golden_filename,
out_filename=test.root + "/docs/gen/ex_FINALDLY_msg.rst",
lines="1-3")
test.passes()

View File

@ -0,0 +1,10 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Antmicro
// SPDX-License-Identifier: CC0-1.0
module t;
bit foo;
final foo <= 1; // <--- Error
endmodule

View File

@ -11,7 +11,7 @@ import vltest_bootstrap
test.scenarios('simulator')
test.compile(verilator_flags2=["--binary -Wno-INITIALDLY"])
test.compile(verilator_flags2=["--binary"])
test.execute()

View File

@ -11,7 +11,7 @@ import vltest_bootstrap
test.scenarios('simulator')
test.compile(verilator_flags2=['-Wno-INITIALDLY'])
test.compile()
test.execute()

View File

@ -1,11 +0,0 @@
%Warning-INITIALDLY: t/t_initial_dlyass.v:17:7: Non-blocking assignment '<=' in initial/final block
: ... This will be executed as a blocking assignment '='!
17 | a <= 22;
| ^~
... For warning description see https://verilator.org/warn/INITIALDLY?v=latest
... Use "/* verilator lint_off INITIALDLY */" and lint_on around source to disable this message.
%Warning-INITIALDLY: t/t_initial_dlyass.v:18:7: Non-blocking assignment '<=' in initial/final block
: ... This will be executed as a blocking assignment '='!
18 | b <= 33;
| ^~
%Error: Exiting due to

View File

@ -40,6 +40,7 @@ module t;
// verilator lint_off ENUMITEMWIDTH
// verilator lint_off ENUMVALUE
// verilator lint_off EOFNEWLINE
// verilator lint_off FINALDLY
// verilator lint_off FUNCTIMECTL
// verilator lint_off GENCLK
// verilator lint_off GENUNNAMED

View File

@ -0,0 +1,25 @@
#!/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.top_filename = "t/t_procedure_nba.v"
test.compile(verilator_flags2=["--binary", "--stats", "-DPROCEDURE=always"])
test.file_grep(test.stats, r'Scheduling, \'act\' extra triggers\s+(\d+)', 0)
test.file_grep(test.stats, r'Scheduling, \'act\' pre triggers\s+(\d+)', 0)
test.file_grep(test.stats, r'Scheduling, \'act\' sense triggers\s+(\d+)', 3)
test.file_grep(test.stats, r'Procedures needing initial NBA trigger\s+(\d+)', 100)
test.execute()
test.passes()

View File

@ -0,0 +1,25 @@
#!/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.top_filename = "t/t_procedure_nba.v"
test.compile(verilator_flags2=["--binary", "--stats", "-DPROCEDURE=initial"])
test.file_grep(test.stats, r'Scheduling, \'act\' extra triggers\s+(\d+)', 0)
test.file_grep(test.stats, r'Scheduling, \'act\' pre triggers\s+(\d+)', 0)
test.file_grep(test.stats, r'Scheduling, \'act\' sense triggers\s+(\d+)', 3)
test.file_grep(test.stats, r'Procedures needing initial NBA trigger\s+(\d+)', 100)
test.execute()
test.passes()

View File

@ -0,0 +1,37 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain
// SPDX-FileCopyrightText: 2026 Antmicro
// SPDX-License-Identifier: CC0-1.0
module t;
logic clk;
initial clk = 0;
always #5 clk = ~clk;
bit [99:0][2:0] foo;
bit bar;
always @(posedge clk) begin
bar <= ~bar;
#1;
end
genvar i;
for (i = 0; i < 100; i=i+1)
`PROCEDURE begin
foo[i] <= 3;
if (foo[i] !== 0) $stop;
@(posedge clk);
if (foo[i] !== 3) $stop;
foo[i] = 2;
if (foo[i] !== 2) $stop;
#1;
if (foo[i] !== 2) $stop;
foo[i] = 0;
end
initial #100 begin
$write("*-* All Finished *-*\n");
$finish;
end
endmodule