Support native FSM state and arc coverage (#7412)

This commit is contained in:
Yogish Sekhar 2026-04-22 20:18:59 +01:00 committed by GitHub
parent 41ddf4d9b6
commit a680919edc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
80 changed files with 3157 additions and 35 deletions

View File

@ -358,6 +358,7 @@ detailed descriptions of these arguments.
--coverage Enable all coverage
--coverage-expr Enable expression coverage
--coverage-expr-max <value> Maximum permutations allowed for an expression
--coverage-fsm Enable FSM state/arc coverage
--coverage-line Enable line coverage
--coverage-max-width <width> Maximum array depth for coverage
--coverage-toggle Enable toggle coverage

View File

@ -175,6 +175,7 @@ L<https://verilator.org/guide/latest/exe_verilator_coverage.html>.
--annotate-points Annotates info from each coverage point.
--filter-type <regex> Keep only records of given coverage type.
--help Displays this message and version and exits.
--include-reset-arcs Include reset arcs in FSM arc summaries.
--rank Compute relative importance of tests.
--unlink With --write, unlink all inputs
--version Displays program version and exits.

View File

@ -281,7 +281,8 @@ Summary:
.. option:: --coverage
Enables all forms of coverage, an alias for :vlopt:`--coverage-line`
:vlopt:`--coverage-toggle` :vlopt:`--coverage-expr` :vlopt:`--coverage-user`.
:vlopt:`--coverage-toggle` :vlopt:`--coverage-expr` :vlopt:`--coverage-fsm`
:vlopt:`--coverage-user`.
.. option:: --coverage-expr
@ -293,6 +294,10 @@ Summary:
covered for a given expression. Defaults to 32. Increasing may slow
coverage simulations and make analyzing the results unwieldy.
.. option:: --coverage-fsm
Enables native FSM state and arc coverage. See :ref:`FSM Coverage`.
.. option:: --coverage-line
Enables basic block line coverage analysis. See :ref:`Line Coverage`.

View File

@ -129,13 +129,20 @@ verilator_coverage Arguments
.. option:: --filter-type <regex>
Skips records of coverage types that matches with <regex>
Possible values are `toggle`, `line`, `branch`, `expr`, `user` and
a wildcard with `\*` or `?`. The default value is `\*`.
Possible values are `toggle`, `line`, `branch`, `expr`, `user`,
`fsm_state`, `fsm_arc` and a wildcard with `\*` or `?`. The default
value is `\*`.
.. option:: --help
Displays a help summary, the program version, and exits.
.. option:: --include-reset-arcs
Includes FSM reset arcs in the printed summaries and annotated output.
By default, reset arcs are tracked but summarized separately from the
non-reset FSM arcs.
.. option:: --rank
Prints an experimental report listing the relative importance of each

View File

@ -185,6 +185,7 @@ SystemVerilog code coverage. With :vlopt:`--coverage`, Verilator enables
all forms of coverage:
- :ref:`User Coverage`
- :ref:`FSM Coverage`
- :ref:`Line Coverage`
- :ref:`Toggle Coverage`
@ -208,6 +209,47 @@ point under the coverage name "DefaultClock":
DefaultClock: cover property (@(posedge clk) cyc==3);
.. _fsm coverage:
FSM Coverage
------------
With :vlopt:`--coverage` or :vlopt:`--coverage-fsm`, Verilator can
instrument a conservative subset of single-process FSMs and report both
state coverage (`fsm_state`) and transition coverage (`fsm_arc`).
This feature is currently experimental and might change in subsequent
releases. In particular, the native FSM coverage extraction heuristics,
:vlopt:`--coverage-fsm`, and the Verilator-specific FSM metacomments below
should be treated as subject to change while the interface settles.
FSM extraction is intentionally narrow. The current implementation targets
clocked, enum-driven state machines that can be recovered directly from the
RTL. It does not claim broad support for two-process FSMs, one-hot
inference, helper-function next-state recovery, or deeply nested control
recovery.
The following metacomments may be attached to the state variable to steer
the extracted coverage model:
- ``/*verilator fsm_state*/`` forces the variable to be treated as
FSM state.
- ``/*verilator fsm_reset_arc*/`` marks reset transitions as
user-visible reset arcs instead of defaulting to a hidden reset-only
summary.
- ``/*verilator fsm_arc_include_cond*/`` keeps conditional branch
arcs that would otherwise be skipped by the conservative extractor.
Reset transitions are included in the collected data either way. By
default, :command:`verilator_coverage` summarizes reset-only arcs rather
than printing them alongside non-reset arcs. Use
:option:`verilator_coverage --include-reset-arcs` to include
those arcs in the printed summary and annotated output.
Annotated output produced by :command:`verilator_coverage --annotate` will
label FSM points with `fsm_state` and `fsm_arc`, and synthetic fallback
transitions with `SYNTHETIC DEFAULT ARC`.
.. _line coverage:

View File

@ -306,7 +306,7 @@ List Of Warnings
else
array[address] <= data;
While this is supported in typical synthesizeable code (including the
While this is supported in typical synthesizable code (including the
example above), some complicated cases are not supported. Namely:
1. If the above loop is inside a suspendable process or fork statement.
@ -837,6 +837,18 @@ List Of Warnings
with a newline."
.. option:: FSMMULTI
Warns that the same always block contains multiple enum-typed case
statements that look like FSM candidates for native FSM coverage when
:vlopt:`--coverage-fsm` or :vlopt:`--coverage` is enabled.
Verilator's FSM coverage instruments only the first such candidate in
source order. Split the FSMs into separate always blocks, or explicitly
annotate the intended state variables and restructure the RTL for full
coverage of such multiple state machines.
.. option:: FUNCTIMECTL
Error that a function contains a time-controlling statement or call of a

View File

@ -2,6 +2,7 @@ ABCp
Aadi
Accellera
Aditya
allocator
Affe
Aleksander
Alexandre
@ -362,6 +363,7 @@ Olofsson
Ondrej
Oron
Oyvind
output
PLI
Pakanati
Palaniappan
@ -402,8 +404,10 @@ Ranjan
Rapp
Redhat
Reitan
reentrant
Renga
Requin
reusability
Riaz
Rodas
Rodionov

View File

@ -499,6 +499,26 @@ void VerilatedCovContext::_insertp(A(0), A(1), A(2), A(3), A(4), A(5), A(6), A(7
C(13), C(14), C(15), C(16), C(17), C(18), C(19), N(20), N(21), N(22), N(23), N(24),
N(25), N(26), N(27), N(28), N(29));
}
// Backward compatibility for mixed inserts with integer-valued
// lineno/column pairs and C-string-valued metadata pairs.
void VerilatedCovContext::_insertp(A(0), A(1), K(2), int val2, K(3), int val3, A(4), A(5), A(6),
A(7)) VL_MT_SAFE {
const std::string val2str = std::to_string(val2);
const std::string val3str = std::to_string(val3);
_insertp(C(0), C(1), key2, val2str.c_str(), key3, val3str.c_str(), C(4), C(5), C(6), C(7),
N(8), N(9), N(10), N(11), N(12), N(13), N(14), N(15), N(16), N(17), N(18), N(19),
N(20), N(21), N(22), N(23), N(24), N(25), N(26), N(27), N(28), N(29));
}
// Backward compatibility for mixed inserts with integer-valued
// lineno/column pairs and additional FSM metadata pairs.
void VerilatedCovContext::_insertp(A(0), A(1), K(2), int val2, K(3), int val3, A(4), A(5), A(6),
A(7), A(8), A(9), A(10), A(11)) VL_MT_SAFE {
const std::string val2str = std::to_string(val2);
const std::string val3str = std::to_string(val3);
_insertp(C(0), C(1), key2, val2str.c_str(), key3, val3str.c_str(), C(4), C(5), C(6), C(7),
C(8), C(9), C(10), C(11), N(12), N(13), N(14), N(15), N(16), N(17), N(18), N(19),
N(20), N(21), N(22), N(23), N(24), N(25), N(26), N(27), N(28), N(29));
}
// Backward compatibility for Verilator
void VerilatedCovContext::_insertp(A(0), A(1), K(2), int val2, K(3), int val3, K(4),
const std::string& val4, A(5), A(6), A(7)) VL_MT_SAFE {

View File

@ -191,6 +191,13 @@ public:
void _insertp(A(0), A(1), A(2), A(3), A(4), A(5), A(6), A(7), A(8), A(9), A(10), A(11), A(12),
A(13), A(14), A(15), A(16), A(17), A(18), A(19), A(20), D(21), D(22), D(23),
D(24), D(25), D(26), D(27), D(28), D(29)) VL_MT_SAFE;
// Backward compatibility for mixed inserts with integer-valued
// lineno/column pairs and C-string-valued metadata pairs.
void _insertp(A(0), A(1), K(2), int val2, K(3), int val3, A(4), A(5), A(6), A(7)) VL_MT_SAFE;
// Backward compatibility for mixed inserts with integer-valued
// lineno/column pairs and additional FSM metadata pairs.
void _insertp(A(0), A(1), K(2), int val2, K(3), int val3, A(4), A(5), A(6), A(7), A(8), A(9),
A(10), A(11)) VL_MT_SAFE;
// Backward compatibility for Verilator
void _insertp(A(0), A(1), K(2), int val2, K(3), int val3, K(4), const std::string& val4, A(5),
A(6), A(7)) VL_MT_SAFE;

View File

@ -40,6 +40,10 @@ VLCOVGEN_ITEM("'name':'thresh', 'short':'s', 'group':1, 'default':None, 'd
VLCOVGEN_ITEM("'name':'type', 'short':'t', 'group':1, 'default':'', 'descr':'Type of coverage (block, line, fsm, etc)'")
// Bin attributes
VLCOVGEN_ITEM("'name':'comment', 'short':'o', 'group':0, 'default':'', 'descr':'Textual description for the item'")
VLCOVGEN_ITEM("'name':'fsm_from', 'short':'Ff', 'group':0, 'default':'', 'descr':'FSM source state name for structured FSM coverage points'")
VLCOVGEN_ITEM("'name':'fsm_tag', 'short':'Fg', 'group':0, 'default':'', 'descr':'FSM point tag such as reset, reset_include, or default'")
VLCOVGEN_ITEM("'name':'fsm_to', 'short':'Ft', 'group':0, 'default':'', 'descr':'FSM destination state name for structured FSM coverage points'")
VLCOVGEN_ITEM("'name':'fsm_var', 'short':'Fv', 'group':0, 'default':'', 'descr':'FSM state variable name for structured FSM coverage points'")
VLCOVGEN_ITEM("'name':'hier', 'short':'h', 'group':0, 'default':'', 'descr':'Hierarchy path name for the item'")
VLCOVGEN_ITEM("'name':'lineno', 'short':'l', 'group':0, 'default':0, 'descr':'Line number for the item'")
VLCOVGEN_ITEM("'name':'weight', 'short':'w', 'group':0, 'default':None, 'descr':'For totaling items, weight of this item'")
@ -49,6 +53,10 @@ VLCOVGEN_ITEM("'name':'weight', 'short':'w', 'group':0, 'default':None, 'd
#define VL_CIK_COLUMN "n"
#define VL_CIK_COMMENT "o"
#define VL_CIK_FILENAME "f"
#define VL_CIK_FSM_FROM "Ff"
#define VL_CIK_FSM_TAG "Fg"
#define VL_CIK_FSM_TO "Ft"
#define VL_CIK_FSM_VAR "Fv"
#define VL_CIK_HIER "h"
#define VL_CIK_LINENO "l"
#define VL_CIK_LINESCOV "S"
@ -70,6 +78,10 @@ public:
if (key == "column") return VL_CIK_COLUMN;
if (key == "comment") return VL_CIK_COMMENT;
if (key == "filename") return VL_CIK_FILENAME;
if (key == "fsm_from") return VL_CIK_FSM_FROM;
if (key == "fsm_tag") return VL_CIK_FSM_TAG;
if (key == "fsm_to") return VL_CIK_FSM_TO;
if (key == "fsm_var") return VL_CIK_FSM_VAR;
if (key == "hier") return VL_CIK_HIER;
if (key == "lineno") return VL_CIK_LINENO;
if (key == "linescov") return VL_CIK_LINESCOV;

View File

@ -100,6 +100,7 @@ set(HEADERS
V3File.h
V3FileLine.h
V3Force.h
V3FsmDetect.h
V3Fork.h
V3FuncOpt.h
V3FunctionTraits.h
@ -277,6 +278,7 @@ set(COMMON_SOURCES
V3File.cpp
V3FileLine.cpp
V3Force.cpp
V3FsmDetect.cpp
V3Fork.cpp
V3FuncOpt.cpp
V3Gate.cpp

View File

@ -280,6 +280,7 @@ RAW_OBJS_PCH_ASTNOMT = \
V3ExecGraph.o \
V3Expand.o \
V3Force.o \
V3FsmDetect.o \
V3Fork.o \
V3Gate.o \
V3HierBlock.o \

View File

@ -311,6 +311,9 @@ public:
//
VAR_BASE, // V3LinkResolve creates for AstPreSel, V3LinkParam removes
VAR_FORCEABLE, // V3LinkParse moves to AstVar::isForceable
VAR_FSM_ARC_INCLUDE_COND, // V3LinkParse moves to AstVar::attrFsmArcInclCond
VAR_FSM_RESET_ARC, // V3LinkParse moves to AstVar::attrFsmResetArc
VAR_FSM_STATE, // V3LinkParse moves to AstVar::attrFsmState
VAR_PORT_DTYPE, // V3LinkDot for V3Width to check port dtype
VAR_PUBLIC, // V3LinkParse moves to AstVar::sigPublic
VAR_PUBLIC_FLAT, // V3LinkParse moves to AstVar::sigPublic
@ -336,10 +339,10 @@ public:
"ENUM_NEXT", "ENUM_PREV", "ENUM_NAME", "ENUM_VALID",
"FUNC_ARG_PROTO", "FUNC_RETURN_PROTO",
"TYPEID", "TYPENAME",
"VAR_BASE", "VAR_FORCEABLE", "VAR_PORT_DTYPE", "VAR_PUBLIC",
"VAR_PUBLIC_FLAT", "VAR_PUBLIC_FLAT_RD", "VAR_PUBLIC_FLAT_RW",
"VAR_ISOLATE_ASSIGNMENTS", "VAR_SC_BIGUINT", "VAR_SC_BV", "VAR_SFORMAT",
"VAR_SPLIT_VAR"
"VAR_BASE", "VAR_FORCEABLE", "VAR_FSM_ARC_INCLUDE_COND", "VAR_FSM_RESET_ARC",
"VAR_FSM_STATE", "VAR_PORT_DTYPE", "VAR_PUBLIC", "VAR_PUBLIC_FLAT",
"VAR_PUBLIC_FLAT_RD", "VAR_PUBLIC_FLAT_RW", "VAR_ISOLATE_ASSIGNMENTS",
"VAR_SC_BIGUINT", "VAR_SC_BV", "VAR_SFORMAT", "VAR_SPLIT_VAR"
};
// clang-format on
return names[m_e];

View File

@ -1934,6 +1934,9 @@ class AstVar final : public AstNode {
bool m_attrIsolateAssign : 1; // User isolate_assignments attribute
bool m_attrSFormat : 1; // User sformat attribute
bool m_attrSplitVar : 1; // declared with split_var metacomment
bool m_attrFsmState : 1; // declared with fsm_state metacomment
bool m_attrFsmResetArc : 1; // declared with fsm_reset_arc metacomment
bool m_attrFsmArcInclCond : 1; // declared with fsm_arc_include_cond metacomment
bool m_fileDescr : 1; // File descriptor
bool m_gotNansiType : 1; // Linker saw Non-ANSI type declaration
bool m_isConst : 1; // Table contains constant data
@ -1992,6 +1995,9 @@ class AstVar final : public AstNode {
m_attrIsolateAssign = false;
m_attrSFormat = false;
m_attrSplitVar = false;
m_attrFsmState = false;
m_attrFsmResetArc = false;
m_attrFsmArcInclCond = false;
m_fileDescr = false;
m_gotNansiType = false;
m_isConst = false;
@ -2136,6 +2142,9 @@ public:
void attrIsolateAssign(bool flag) { m_attrIsolateAssign = flag; }
void attrSFormat(bool flag) { m_attrSFormat = flag; }
void attrSplitVar(bool flag) { m_attrSplitVar = flag; }
void attrFsmState(bool flag) { m_attrFsmState = flag; }
void attrFsmResetArc(bool flag) { m_attrFsmResetArc = flag; }
void attrFsmArcInclCond(bool flag) { m_attrFsmArcInclCond = flag; }
void rand(const VRandAttr flag) { m_rand = flag; }
void usedParam(bool flag) { m_usedParam = flag; }
void usedLoopIdx(bool flag) { m_usedLoopIdx = flag; }
@ -2299,6 +2308,9 @@ public:
bool attrFileDescr() const { return m_fileDescr; }
bool attrSFormat() const { return m_attrSFormat; }
bool attrSplitVar() const { return m_attrSplitVar; }
bool attrFsmState() const { return m_attrFsmState; }
bool attrFsmResetArc() const { return m_attrFsmResetArc; }
bool attrFsmArcInclCond() const { return m_attrFsmArcInclCond; }
bool attrIsolateAssign() const { return m_attrIsolateAssign; }
AstIface* sensIfacep() const { return m_sensIfacep; }
VRandAttr rand() const { return m_rand; }
@ -2384,12 +2396,22 @@ class AstCoverOtherDecl final : public AstNodeCoverDecl {
// Coverage analysis point declaration
// Used for other than toggle types of coverage
string m_linescov;
string m_fsmVar;
string m_fsmFrom;
string m_fsmTo;
string m_fsmTag;
int m_offset; // Offset column numbers to uniq-ify IFs
public:
AstCoverOtherDecl(FileLine* fl, const string& page, const string& comment,
const string& linescov, int offset)
const string& linescov, int offset, const string& fsmVar = "",
const string& fsmFrom = "", const string& fsmTo = "",
const string& fsmTag = "")
: ASTGEN_SUPER_CoverOtherDecl(fl, page, comment)
, m_linescov{linescov}
, m_fsmVar{fsmVar}
, m_fsmFrom{fsmFrom}
, m_fsmTo{fsmTo}
, m_fsmTag{fsmTag}
, m_offset{offset} {}
ASTGEN_MEMBERS_AstCoverOtherDecl;
void dump(std::ostream& str) const override;
@ -2397,6 +2419,10 @@ public:
int offset() const { return m_offset; }
int size() const override { return 1; }
const string& linescov() const { return m_linescov; }
const string& fsmVar() const { return m_fsmVar; }
const string& fsmFrom() const { return m_fsmFrom; }
const string& fsmTo() const { return m_fsmTo; }
const string& fsmTag() const { return m_fsmTag; }
bool sameNode(const AstNode* samep) const override {
const AstCoverOtherDecl* const asamep = VN_DBG_AS(samep, CoverOtherDecl);
return AstNodeCoverDecl::sameNode(samep) && linescov() == asamep->linescov();

View File

@ -3004,6 +3004,9 @@ void AstVar::dump(std::ostream& str) const {
if (processQueue()) str << " [PROCQ]";
if (sampled()) str << " [SAMPLED]";
if (attrIsolateAssign()) str << " [aISO]";
if (attrFsmState()) str << " [aFSMSTATE]";
if (attrFsmResetArc()) str << " [aFSMRESETARC]";
if (attrFsmArcInclCond()) str << " [aFSMARCCOND]";
if (attrFileDescr()) str << " [aFD]";
if (isFuncReturn()) {
str << " [FUNCRTN]";
@ -3036,6 +3039,9 @@ void AstVar::dumpJson(std::ostream& str) const {
dumpJsonBoolFuncIf(str, processQueue);
dumpJsonBoolFuncIf(str, sampled);
dumpJsonBoolFuncIf(str, attrIsolateAssign);
dumpJsonBoolFuncIf(str, attrFsmState);
dumpJsonBoolFuncIf(str, attrFsmResetArc);
dumpJsonBoolFuncIf(str, attrFsmArcInclCond);
dumpJsonBoolFuncIf(str, attrFileDescr);
dumpJsonBoolFuncIf(str, isDpiOpenArray);
dumpJsonBoolFuncIf(str, isFuncReturn);
@ -3283,10 +3289,18 @@ void AstNodeCoverDecl::dumpJson(std::ostream& str) const {
void AstCoverOtherDecl::dump(std::ostream& str) const {
this->AstNodeCoverDecl::dump(str);
if (!linescov().empty()) str << " lc=" << linescov();
if (!fsmVar().empty()) str << " fv=" << fsmVar();
if (!fsmFrom().empty()) str << " ff=" << fsmFrom();
if (!fsmTo().empty()) str << " ft=" << fsmTo();
if (!fsmTag().empty()) str << " fg=" << fsmTag();
}
void AstCoverOtherDecl::dumpJson(std::ostream& str) const {
this->AstNodeCoverDecl::dumpJson(str);
dumpJsonStrFunc(str, linescov);
dumpJsonStrFunc(str, fsmVar);
dumpJsonStrFunc(str, fsmFrom);
dumpJsonStrFunc(str, fsmTo);
dumpJsonStrFunc(str, fsmTag);
}
void AstCoverToggleDecl::dump(std::ostream& str) const {
this->AstNodeCoverDecl::dump(str);

View File

@ -826,6 +826,14 @@ public:
putsQuoted(VIdProtect::protectWordsIf(nodep->comment(), nodep->protect()));
puts(", ");
putsQuoted(nodep->linescov());
puts(", ");
putsQuoted(VIdProtect::protectWordsIf(nodep->fsmVar(), nodep->protect()));
puts(", ");
putsQuoted(VIdProtect::protectWordsIf(nodep->fsmFrom(), nodep->protect()));
puts(", ");
putsQuoted(VIdProtect::protectWordsIf(nodep->fsmTo(), nodep->protect()));
puts(", ");
putsQuoted(VIdProtect::protectWordsIf(nodep->fsmTag(), nodep->protect()));
puts(");\n");
}
void visit(AstCoverToggleDecl* nodep) override {

View File

@ -197,7 +197,9 @@ class EmitCHeader final : public EmitCConstInit {
puts(v3Global.opt.threads() > 1 ? "std::atomic<uint32_t>" : "uint32_t");
puts("* countp, bool enable, const char* filenamep, int lineno, int column,\n");
puts("const char* hierp, const char* pagep, const char* commentp, const char* "
"linescovp);\n");
"linescovp,\n");
puts("const char* fsmVarp, const char* fsmFromp, const char* fsmTop, const char* "
"fsmTagp);\n");
}
if (v3Global.opt.coverageToggle() && !VN_IS(modp, Class)) {

View File

@ -180,7 +180,9 @@ class EmitCImp final : public EmitCFunc {
puts(v3Global.opt.threads() > 1 ? "std::atomic<uint32_t>" : "uint32_t");
puts("* countp, bool enable, const char* filenamep, int lineno, int column,\n");
puts("const char* hierp, const char* pagep, const char* commentp, const char* "
"linescovp) {\n");
"linescovp,\n");
puts("const char* fsmVarp, const char* fsmFromp, const char* fsmTop, const char* "
"fsmTagp) {\n");
if (v3Global.opt.threads() > 1) {
puts("assert(sizeof(uint32_t) == sizeof(std::atomic<uint32_t>));\n");
puts("uint32_t* count32p = reinterpret_cast<uint32_t*>(countp);\n");
@ -198,10 +200,14 @@ class EmitCImp final : public EmitCFunc {
puts(" \"filename\",filenamep,");
puts(" \"lineno\",lineno,");
puts(" \"column\",column,\n");
puts("\"hier\",fullhier,");
puts("\"hier\",fullhier.c_str(),");
puts(" \"page\",pagep,");
puts(" \"comment\",commentp,");
puts(" (linescovp[0] ? \"linescov\" : \"\"), linescovp);\n");
puts(" (linescovp[0] ? \"linescov\" : \"\"), linescovp,");
puts(" (fsmVarp[0] ? \"fsm_var\" : \"\"), fsmVarp,");
puts(" (fsmFromp[0] ? \"fsm_from\" : \"\"), fsmFromp,");
puts(" (fsmTop[0] ? \"fsm_to\" : \"\"), fsmTop,");
puts(" (fsmTagp[0] ? \"fsm_tag\" : \"\"), fsmTagp);\n");
puts("}\n");
}
if (v3Global.opt.coverageToggle()) {
@ -237,7 +243,7 @@ class EmitCImp final : public EmitCFunc {
puts(" \"filename\",filenamep,");
puts(" \"lineno\",lineno,");
puts(" \"column\",column,\n");
puts("\"hier\",fullhier,");
puts("\"hier\",fullhier.c_str(),");
puts(" \"page\",pagep,");
puts(" \"comment\",commentWithIndex.c_str(),");
puts(" \"\", \"\");\n"); // linescov argument, but in toggle coverage it is always

View File

@ -106,6 +106,7 @@ public:
ENUMITEMWIDTH, // Error: enum item width mismatch
ENUMVALUE, // Error: enum type needs explicit cast
EOFNEWLINE, // End-of-file missing newline
FSMMULTI, // Multiple FSM candidates in one always block
FUNCTIMECTL, // Functions cannot have timing/delay/wait
FUTURE, // Feature is under development and not yet supported
GENCLK, // Generated Clock. Historical, never issued.
@ -223,16 +224,16 @@ public:
"BSSPACE", "CASEINCOMPLETE", "CASEOVERLAP", "CASEWITHX", "CASEX", "CASTCONST",
"CDCRSTLOGIC", "CLKDATA", "CMPCONST", "COLONPLUS", "COMBDLY", "CONSTRAINTIGN",
"CONTASSREG", "COVERIGN", "DECLFILENAME", "DEFOVERRIDE", "DEFPARAM", "DEPRECATED",
"ENCAPSULATED", "ENDLABEL", "ENUMITEMWIDTH", "ENUMVALUE", "EOFNEWLINE", "FUNCTIMECTL",
"FUTURE", "GENCLK", "GENUNNAMED", "HIERBLOCK", "HIERPARAM", "IFDEPTH", "IGNOREDRETURN",
"IMPERFECTSCH", "IMPLICIT", "IMPLICITSTATIC", "IMPORTSTAR", "IMPURE", "INCABSPATH",
"INFINITELOOP", "INITIALDLY", "INSECURE", "INSIDETRUE", "LATCH", "LITENDIAN",
"MINTYPMAXDLY", "MISINDENT", "MODDUP", "MODMISSING", "MULTIDRIVEN", "MULTITOP",
"NEWERSTD", "NOEFFECT", "NOLATCH", "NONSTD", "NORETURN", "NULLPORT", "PARAMNODEFAULT",
"PINCONNECTEMPTY", "PINMISSING", "PINNOCONNECT", "PINNOTFOUND", "PKGNODECL",
"PREPROCZERO", "PROCASSINIT", "PROCASSWIRE", "PROFOUTOFDATE", "PROTECTED",
"ENCAPSULATED", "ENDLABEL", "ENUMITEMWIDTH", "ENUMVALUE", "EOFNEWLINE", "FSMMULTI",
"FUNCTIMECTL", "FUTURE", "GENCLK", "GENUNNAMED", "HIERBLOCK", "HIERPARAM", "IFDEPTH",
"IGNOREDRETURN", "IMPERFECTSCH", "IMPLICIT", "IMPLICITSTATIC", "IMPORTSTAR", "IMPURE",
"INCABSPATH","INFINITELOOP", "INITIALDLY", "INSECURE", "INSIDETRUE", "LATCH",
"LITENDIAN", "MINTYPMAXDLY", "MISINDENT", "MODDUP", "MODMISSING", "MULTIDRIVEN",
"MULTITOP", "NEWERSTD", "NOEFFECT", "NOLATCH", "NONSTD", "NORETURN", "NULLPORT",
"PARAMNODEFAULT", "PINCONNECTEMPTY", "PINMISSING", "PINNOCONNECT", "PINNOTFOUND",
"PKGNODECL", "PREPROCZERO", "PROCASSINIT", "PROCASSWIRE", "PROFOUTOFDATE", "PROTECTED",
"PROTOTYPEMIS", "RANDC", "REALCVT", "REDEFMACRO", "RISEFALLDLY", "SELRANGE",
"SHORTREAL", "SIDEEFFECT", "SPECIFYIGN", "SPLITVAR", "STATICVAR", "STMTDLY",
"SHORTREAL", "SIDEEFFECT", "SPECIFYIGN", "SPLITVAR", "STATICVAR","STMTDLY",
"SUPERNFIRST", "SYMRSVDWORD", "SYNCASYNCNET", "TICKCOUNT", "TIMESCALEMOD", "UNDRIVEN",
"UNOPT", "UNOPTFLAT", "UNOPTTHREADS", "UNPACKED", "UNSATCONSTR", "UNSIGNED", "UNUSED",
"UNUSEDGENVAR", "UNUSEDLOOP", "UNUSEDPARAM", "UNUSEDSIGNAL", "USERERROR", "USERFATAL",

785
src/V3FsmDetect.cpp Normal file
View File

@ -0,0 +1,785 @@
// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: FSM coverage detect pass
//
// Code available from: https://verilator.org
//
//*************************************************************************
//
// 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
//
//*************************************************************************
// FSM COVERAGE DETECT:
// Walk clocked always blocks while the original FSM structure is still
// present, build a per-FSM V3Graph representation of the extracted
// states/transitions, then immediately lower that completed graph state
// into the final coverage declarations, previous-state tracking, and
// active blocks needed to implement FSM state and arc coverage in the
// generated model.
//
//*************************************************************************
#include "V3PchAstNoMT.h"
#include "V3FsmDetect.h"
#include "V3Ast.h"
#include "V3Graph.h"
#include <cctype>
#include <map>
#include <memory>
#include <unordered_map>
VL_DEFINE_DEBUG_FUNCTIONS;
namespace {
// Captures one sensitivity-list entry so the lowering phase can later rebuild
// an active block with the same triggering event control.
struct FsmSenDesc final {
// Encoded edge kind copied from AstSenItem::edgeType() so lowering can
// rebuild the same trigger semantics on the synthesized coverage block.
VEdgeType::en edgeType = static_cast<VEdgeType::en>(0);
// Triggering signal in the saved scoped AST.
AstVarScope* varScopep = nullptr;
};
// Captures the simple reset predicate shape that survives to this pass after
// earlier normalization so reset arcs can be reconstructed during lowering.
struct FsmResetCondDesc final {
// Reset signal used by the FSM in the saved scoped AST.
AstVarScope* varScopep = nullptr;
};
class FsmGraph;
class FsmVertex VL_NOT_FINAL : public V3GraphVertex {
VL_RTTI_IMPL(FsmVertex, V3GraphVertex)
public:
enum class Kind : uint8_t { STATE, RESET_ANY, DEFAULT_ANY };
private:
Kind m_kind; // State vs synthetic ANY/default vertex role.
string m_label; // User-facing state or pseudo-state label.
int m_value = 0; // Encoded state value for real state vertices.
protected:
FsmVertex(V3Graph* graphp, Kind kind, string label, int value) VL_MT_DISABLED
: V3GraphVertex{graphp}
, m_kind{kind}
, m_label{label}
, m_value{value} {}
~FsmVertex() override = default;
public:
Kind kind() const { return m_kind; }
bool isState() const { return m_kind == Kind::STATE; }
bool isResetAny() const { return m_kind == Kind::RESET_ANY; }
bool isDefaultAny() const { return m_kind == Kind::DEFAULT_ANY; }
const string& label() const { return m_label; }
int value() const { return m_value; }
string name() const override VL_MT_SAFE { return m_label + "=" + cvtToStr(m_value); }
};
class FsmStateVertex final : public FsmVertex {
VL_RTTI_IMPL(FsmStateVertex, FsmVertex)
public:
FsmStateVertex(V3Graph* graphp, string label, int value) VL_MT_DISABLED
: FsmVertex{graphp, Kind::STATE, label, value} {}
~FsmStateVertex() override = default;
string dotColor() const override { return "lightblue"; }
string dotShape() const override { return "ellipse"; }
};
class FsmPseudoVertex final : public FsmVertex {
VL_RTTI_IMPL(FsmPseudoVertex, FsmVertex)
public:
FsmPseudoVertex(V3Graph* graphp, Kind kind, string label) VL_MT_DISABLED
: FsmVertex{graphp, kind, label, 0} {}
~FsmPseudoVertex() override = default;
string name() const override VL_MT_SAFE { return label(); }
string dotColor() const override { return isResetAny() ? "darkgreen" : "orange"; }
string dotShape() const override { return "diamond"; }
};
class FsmArcEdge final : public V3GraphEdge {
VL_RTTI_IMPL(FsmArcEdge, V3GraphEdge)
bool m_isReset = false; // Arc originates from the synthetic reset source.
bool m_isCond = false; // Arc came from a conditional next-state split.
bool m_isDefault = false; // Arc represents a case default source.
FileLine* m_flp = nullptr; // Source location for emitted coverage metadata.
public:
FsmArcEdge(V3Graph* graphp, FsmVertex* fromp, FsmStateVertex* top, bool isReset,
bool isCond, bool isDefault, FileLine* flp) VL_MT_DISABLED
: V3GraphEdge{graphp, fromp, top, 1}
, m_isReset{isReset}
, m_isCond{isCond}
, m_isDefault{isDefault}
, m_flp{flp} {}
~FsmArcEdge() override = default;
bool isReset() const { return m_isReset; }
bool isCond() const { return m_isCond; }
bool isDefault() const { return m_isDefault; }
FileLine* fileline() const { return m_flp; }
string dotLabel() const override {
if (m_isReset) return "reset";
if (m_isDefault) return "default";
if (m_isCond) return "cond";
return "";
}
string dotColor() const override {
if (m_isReset) return "darkgreen";
if (m_isDefault) return "orange";
if (m_isCond) return "blue";
return "black";
}
};
// One graph per detected FSM. Graph-level metadata captures the non-graph
// context needed to lower states/arcs back into the AST after detection.
class FsmGraph final : public V3Graph {
AstScope* m_scopep = nullptr; // Owning scoped block for the detected FSM.
AstAlways* m_alwaysp = nullptr; // Original always block being instrumented.
string m_stateVarName; // Pretty state variable name for user-visible output.
string m_stateVarInternalName; // Internal state symbol name for dump tags.
AstVarScope* m_stateVarScopep = nullptr; // Scoped state variable being tracked.
std::vector<FsmSenDesc> m_senses; // Saved event controls for recreated active blocks.
FsmResetCondDesc m_resetCond; // Saved reset predicate shape, if one exists.
bool m_hasResetCond = false; // Whether the detected FSM had a reset branch.
bool m_resetInclude = false; // Whether reset arcs count toward coverage totals.
bool m_inclCond = false; // Whether conditional arcs should be kept explicitly.
FileLine* m_flp = nullptr; // Representative source location for declarations/arcs.
std::unordered_map<int, FsmStateVertex*> m_stateVertices; // Value to state-vertex map.
FsmPseudoVertex* m_resetVertexp = nullptr; // Synthetic ANY source for reset arcs.
FsmPseudoVertex* m_defaultVertexp = nullptr; // Synthetic default source for case defaults.
public:
FsmGraph() VL_MT_DISABLED
: m_resetVertexp{new FsmPseudoVertex{this, FsmVertex::Kind::RESET_ANY, "ANY"}}
, m_defaultVertexp{new FsmPseudoVertex{this, FsmVertex::Kind::DEFAULT_ANY, "default"}} {}
AstScope* scopep() const { return m_scopep; }
void scopep(AstScope* scopep) { m_scopep = scopep; }
AstAlways* alwaysp() const { return m_alwaysp; }
void alwaysp(AstAlways* alwaysp) { m_alwaysp = alwaysp; }
const string& stateVarName() const { return m_stateVarName; }
void stateVarName(const string& name) { m_stateVarName = name; }
const string& stateVarInternalName() const { return m_stateVarInternalName; }
void stateVarInternalName(const string& name) { m_stateVarInternalName = name; }
AstVarScope* stateVarScopep() const { return m_stateVarScopep; }
void stateVarScopep(AstVarScope* vscp) { m_stateVarScopep = vscp; }
const std::vector<FsmSenDesc>& senses() const { return m_senses; }
std::vector<FsmSenDesc>& senses() { return m_senses; }
const FsmResetCondDesc& resetCond() const { return m_resetCond; }
FsmResetCondDesc& resetCond() { return m_resetCond; }
bool hasResetCond() const { return m_hasResetCond; }
void hasResetCond(bool flag) { m_hasResetCond = flag; }
bool resetInclude() const { return m_resetInclude; }
void resetInclude(bool flag) { m_resetInclude = flag; }
bool inclCond() const { return m_inclCond; }
void inclCond(bool flag) { m_inclCond = flag; }
FileLine* fileline() const { return m_flp; }
void fileline(FileLine* flp) { m_flp = flp; }
FsmStateVertex* addStateVertex(string label, int value) VL_MT_DISABLED {
FsmStateVertex* const vertexp = new FsmStateVertex{this, label, value};
m_stateVertices.emplace(value, vertexp);
return vertexp;
}
FsmPseudoVertex* resetAnyVertex() VL_MT_DISABLED { return m_resetVertexp; }
FsmPseudoVertex* defaultAnyVertex() VL_MT_DISABLED { return m_defaultVertexp; }
FsmArcEdge* addArc(int fromValue, int toValue, bool isReset, bool isCond, bool isDefault,
FileLine* flp) VL_MT_DISABLED {
FsmStateVertex* const top = m_stateVertices.at(toValue);
FsmVertex* fromp = nullptr;
if (isReset) {
fromp = resetAnyVertex();
} else if (isDefault) {
fromp = defaultAnyVertex();
} else {
fromp = m_stateVertices.at(fromValue);
}
return new FsmArcEdge{this, fromp, top, isReset, isCond, isDefault, flp};
}
string name() const VL_MT_SAFE {
return "FSM "
+ (m_stateVarName.empty() ? (m_stateVarScopep ? m_stateVarScopep->name() : "")
: m_stateVarName);
}
string dumpTag(size_t index) const {
string tag = stateVarInternalName();
for (char& ch : tag) {
if (!std::isalnum(static_cast<unsigned char>(ch))) ch = '_';
}
return "fsm_" + cvtToStr(index) + "_" + tag;
}
};
struct DetectedFsm final {
std::unique_ptr<FsmGraph> graphp; // Extracted graph for one detected FSM candidate.
};
using DetectedFsmMap = std::map<string, DetectedFsm>;
// Local shared state between the two adjacent FSM coverage phases. Detection
// fills this with recovered FSM graphs; lowering consumes the completed graphs
// immediately afterward without needing any AST serialization bridge.
class FsmState final {
// All detected FSMs keyed by state varscope name. This is the only bridge
// between the adjacent detect and lower phases, so the second phase never
// needs to rediscover or serialize the extracted machine.
DetectedFsmMap m_fsms;
public:
DetectedFsmMap& fsms() { return m_fsms; }
const DetectedFsmMap& fsms() const { return m_fsms; }
};
// Detection runs while the original clocked/case structure is still intact and
// populates graph-backed FSM models without mutating the tree mid-traversal.
// This pass is intentionally conservative: for this PR we only lock down the
// small set of transition/selector forms that are already stable in the
// normalized AST we see here. The remaining reject branches are therefore
// mostly future-feature boundaries, not accidental dead code.
class FsmDetectVisitor final : public VNVisitor {
// STATE - for current visit position (use VL_RESTORER)
FsmState& m_state;
AstScope* m_scopep = nullptr;
// METHODS
// Enum-backed FSMs may be wrapped in refs/typedefs; normalize to the
// underlying enum type before deciding whether a case is a candidate.
static AstNodeDType* unwrapEnumCandidate(AstNodeDType* dtypep) {
return dtypep->skipRefToEnump();
}
// Reset arcs are only modeled for the simple signal form that survives to
// this pass after earlier normalization.
static bool isSimpleResetCond(AstNodeExpr* condp) {
return VN_IS(condp, VarRef);
}
// Normalize the reset condition into a compact description so the lowering
// phase can regenerate the same predicate after detection. By the time
// this pass runs, active-low source forms such as "!rst_n" have already
// been canonicalized to a positive-condition if/else shape, so only a
// plain VarRef survives here.
static FsmResetCondDesc describeResetCond(AstNodeExpr* condp) {
FsmResetCondDesc desc;
if (AstVarRef* const vrefp = VN_CAST(condp, VarRef)) {
desc.varScopep = vrefp->varScopep();
}
return desc;
}
// Snapshot the original event control so the lowering phase can rebuild an
// active block with the same edge semantics.
static std::vector<FsmSenDesc> describeSenTree(AstSenTree* sentreep) {
std::vector<FsmSenDesc> senses;
for (AstSenItem* itemp = sentreep->sensesp(); itemp;
itemp = VN_AS(itemp->nextp(), SenItem)) {
AstNodeVarRef* const vrefp = itemp->varrefp();
if (!vrefp) continue;
FsmSenDesc desc;
desc.edgeType = itemp->edgeType().m_e;
desc.varScopep = vrefp->varScopep();
senses.push_back(desc);
}
return senses;
}
// Ignore existing coverage increments so FSM detection sees the user logic
// rather than other instrumentation already attached to the block.
static bool isIgnorableStmt(AstNode* nodep) { return VN_IS(nodep, CoverInc); }
// Conservative extractor: only treat a branch as simple when exactly one
// non-coverage statement remains after unwrapping. Richer multi-statement
// or control-flow forms are intentionally left for follow-on FSM-detection
// work instead of being partially inferred here.
static AstNode* singleMeaningfulStmt(AstNode* stmtp) {
AstNode* resultp = nullptr;
for (AstNode* nodep = stmtp; nodep; nodep = nodep->nextp()) {
if (isIgnorableStmt(nodep)) continue;
if (resultp) return nullptr;
resultp = nodep;
}
return resultp;
}
// Recognize the direct "state <= X" form that gives us an unambiguous arc
// target without needing deeper control-flow reasoning. Branches that fall
// out here represent currently unsupported next-state shapes rather than
// bugs in the implemented subset.
static AstNodeAssign* directStateAssign(AstNode* stmtp, AstVarScope* stateVscp) {
AstNode* const nodep = singleMeaningfulStmt(stmtp);
if (!nodep) return nullptr;
AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign);
if (!assp) return nullptr;
AstVarRef* const vrefp = VN_CAST(assp->lhsp(), VarRef);
if (!vrefp || vrefp->varScopep() != stateVscp) return nullptr;
return assp;
}
// Prefer enum labels in reports; fall back to synthetic labels for forced
// non-enum FSMs so coverage points remain human-readable.
static string labelForValue(const std::unordered_map<int, string>& labels, int value) {
const std::unordered_map<int, string>::const_iterator it = labels.find(value);
return it == labels.end() ? ("S" + cvtToStr(value)) : it->second;
}
// The extractor only models constant-valued state transitions, and by the
// time detect runs those values have already been constant-folded.
static bool exprConstValue(AstNodeExpr* exprp, int& value) {
if (AstConst* const constp = VN_CAST(exprp, Const)) {
value = constp->toSInt();
return true;
}
return false;
}
// Enum-backed FSMs should only transition to values that were interned as
// known states. If a constant transition targets some other encoding, warn
// and skip FSM instrumentation for that edge rather than silently dropping
// it or turning optional coverage into a hard compile failure.
static bool validateKnownStateValue(AstNode* nodep,
const std::unordered_map<int, string>& labels, int value) {
if (labels.find(value) != labels.end()) return true;
nodep->v3warn(COVERIGN,
"Ignoring unsupported: FSM coverage on enum state transitions "
"that assign a constant not present in the declared enum");
return false;
}
// Extract supported case-item transitions in one place so the conservative
// policy for direct and ternary forms stays consistent. The false exits in
// this helper are deliberate subset boundaries: they document shapes we do
// not yet model in this PR and that future FSM-detection work may widen.
static bool emitCaseItemArcs(FsmGraph& graph, AstCaseItem* itemp, AstVarScope* stateVscp,
const std::unordered_map<int, string>& labels, bool inclCond) {
std::vector<std::pair<string, int>> froms;
if (itemp->isDefault()) {
if (!inclCond) return false;
froms.emplace_back("default", 0);
} else {
for (AstNodeExpr* condp = itemp->condsp(); condp;
condp = VN_CAST(condp->nextp(), NodeExpr)) {
int value = 0;
if (!exprConstValue(condp, value)) continue;
froms.emplace_back(labelForValue(labels, value), value);
}
if (froms.empty()) return false;
}
if (AstNodeAssign* const assp = directStateAssign(itemp->stmtsp(), stateVscp)) {
int toValue = 0;
if (exprConstValue(assp->rhsp(), toValue)) {
if (!validateKnownStateValue(assp, labels, toValue)) return true;
for (const std::pair<string, int>& from : froms) {
graph.addArc(from.second, toValue, false, false, itemp->isDefault(),
assp->fileline());
}
return true;
}
if (AstCond* const condp = VN_CAST(assp->rhsp(), Cond)) {
int thenValue = 0;
int elseValue = 0;
const bool simpleCond = exprConstValue(condp->thenp(), thenValue)
&& exprConstValue(condp->elsep(), elseValue);
if (simpleCond || inclCond) {
if (!validateKnownStateValue(condp->thenp(), labels, thenValue)) return true;
if (!validateKnownStateValue(condp->elsep(), labels, elseValue)) return true;
for (const int branchValue : {thenValue, elseValue}) {
for (const std::pair<string, int>& from : froms) {
graph.addArc(from.second, branchValue, false, true,
itemp->isDefault(), assp->fileline());
}
}
return true;
}
}
}
return false;
}
// Reset transitions are described separately because they live in the reset
// branch outside the steady-state case statement.
static void addResetArcs(FsmGraph& graph, AstNode* stmtsp, AstVarScope* stateVscp,
const std::unordered_map<int, string>& labels) {
for (AstNode* nodep = stmtsp; nodep; nodep = nodep->nextp()) {
if (AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign)) {
AstVarRef* const vrefp = VN_CAST(assp->lhsp(), VarRef);
int toValue = 0;
if (vrefp && vrefp->varScopep() == stateVscp && exprConstValue(assp->rhsp(), toValue)) {
if (!validateKnownStateValue(assp, labels, toValue)) continue;
graph.addArc(0, toValue, true, false, false, assp->fileline());
}
}
}
}
// Turn one candidate case statement into the graph representation that the
// later lowering phase will consume directly, while reviewers can still
// inspect the extracted machine via DOT dumps.
void processCase(AstCase* casep, AstNodeExpr* resetCondp, AstAlways* alwaysp) {
AstVarRef* const selp = VN_CAST(casep->exprp(), VarRef);
if (!selp) return;
AstVarScope* const stateVscp = selp->varScopep();
AstVar* const stateVarp = selp->varp();
AstEnumDType* enump = VN_CAST(unwrapEnumCandidate(stateVscp->dtypep()), EnumDType);
if (!enump) enump = VN_CAST(unwrapEnumCandidate(stateVarp->dtypep()), EnumDType);
const bool forced = stateVarp->attrFsmState();
if (!enump && !forced) return;
std::vector<std::pair<string, int>> states;
std::unordered_map<int, string> labels;
if (enump) {
if (stateVscp->width() < 1 || stateVscp->width() > 32) {
casep->v3warn(COVERIGN,
"Ignoring unsupported: FSM coverage on enum-typed state "
"variables wider than 32 bits");
return;
}
for (AstEnumItem* itemp = enump->itemsp(); itemp; itemp = VN_AS(itemp->nextp(), EnumItem)) {
const AstConst* const constp = VN_AS(itemp->valuep(), Const);
const int value = constp->toSInt();
states.emplace_back(itemp->name(), value);
labels.emplace(value, itemp->name());
}
if (states.size() < 2) return;
} else {
const int width = stateVarp->width();
if (width < 1 || width >= 31) return;
const unsigned stateCount = 1U << width;
for (unsigned value = 0; value < stateCount; ++value) {
const string label = "S" + cvtToStr(value);
states.emplace_back(label, static_cast<int>(value));
labels.emplace(static_cast<int>(value), label);
}
}
DetectedFsm& entry = m_state.fsms()[stateVscp->name()];
if (!entry.graphp) {
entry.graphp.reset(new FsmGraph{});
entry.graphp->scopep(m_scopep);
entry.graphp->alwaysp(alwaysp);
entry.graphp->stateVarName(stateVscp->prettyName());
entry.graphp->stateVarInternalName(stateVarp->name());
entry.graphp->stateVarScopep(stateVscp);
entry.graphp->senses() = describeSenTree(alwaysp->sentreep());
entry.graphp->resetCond() = describeResetCond(resetCondp);
entry.graphp->hasResetCond(entry.graphp->resetCond().varScopep != nullptr);
entry.graphp->resetInclude(stateVarp->attrFsmResetArc());
entry.graphp->inclCond(stateVarp->attrFsmArcInclCond());
entry.graphp->fileline(casep->fileline());
for (const std::pair<string, int>& state : states) {
entry.graphp->addStateVertex(state.first, state.second);
}
}
for (AstCaseItem* itemp = casep->itemsp(); itemp; itemp = VN_AS(itemp->nextp(), CaseItem)) {
emitCaseItemArcs(*entry.graphp, itemp, stateVscp, labels, entry.graphp->inclCond());
}
}
// Find the first supported FSM candidate in a clocked always block, warn on
// additional candidates, and attach reset arcs when present. Candidate
// filtering stays narrow on purpose: we prefer to skip ambiguous shapes now
// and expand detection in a later PR rather than over-infer coverage from
// forms we do not yet model confidently.
void processAlways(AstAlways* alwaysp) {
if (!alwaysp->sentreep() || !alwaysp->sentreep()->hasClocked()) return;
std::vector<std::pair<AstCase*, AstNodeExpr*>> candidates;
AstNode* stmtsp = alwaysp->stmtsp();
AstIf* const firstIfp = VN_CAST(stmtsp, If);
if (firstIfp) {
if (AstCase* const casep = VN_CAST(firstIfp->elsesp(), Case)) {
candidates.emplace_back(casep, isSimpleResetCond(firstIfp->condp()) ? firstIfp->condp()
: nullptr);
}
}
for (AstNode* nodep = stmtsp; nodep; nodep = nodep->nextp()) {
if (AstCase* const casep = VN_CAST(nodep, Case)) candidates.emplace_back(casep, nullptr);
}
if (candidates.empty()) return;
AstVarScope* firstVscp = nullptr;
for (const std::pair<AstCase*, AstNodeExpr*>& cand : candidates) {
AstVarRef* const selp = VN_CAST(cand.first->exprp(), VarRef);
AstVarScope* const vscp = selp ? selp->varScopep() : nullptr;
if (!vscp) continue;
if (!firstVscp) {
firstVscp = vscp;
processCase(cand.first, cand.second, alwaysp);
} else if (vscp != firstVscp) {
cand.first->v3warn(FSMMULTI,
"FSM coverage: multiple enum-typed case statements found in "
"the same always block. Only the first candidate will be "
"instrumented.");
} else {
cand.first->v3warn(
COVERIGN,
"Ignoring unsupported: FSM coverage on multiple supported case "
"statements found in the same always block. Only the first "
"candidate will be instrumented.");
}
}
if (!(firstIfp && firstVscp)) return;
const DetectedFsmMap& fsms = m_state.fsms();
const DetectedFsmMap::const_iterator it = fsms.find(firstVscp->name());
if (it == fsms.end()) return;
FsmGraph* const graphp = it->second.graphp.get();
if (!graphp->hasResetCond()) return;
std::unordered_map<int, string> labels;
for (const V3GraphVertex& vtx : graphp->vertices()) {
const FsmVertex* const vertexp = vtx.as<FsmVertex>();
if (!vertexp->isState()) continue;
labels.emplace(vertexp->value(), vertexp->label());
}
addResetArcs(*graphp, firstIfp->thensp(), firstVscp, labels);
}
// Track the current scope so each detected FSM records the module/scope
// where instrumentation must later be inserted.
void visit(AstScope* nodep) override {
VL_RESTORER(m_scopep);
m_scopep = nodep;
iterateChildren(nodep);
}
// FSM extraction only cares about clocked always processes.
void visit(AstAlways* nodep) override { processAlways(nodep); }
// Continue the walk through the rest of the design hierarchy.
void visit(AstNode* nodep) override { iterateChildren(nodep); }
public:
// CONSTRUCTORS
// Collect all FSM graphs into the shared local state before the lowering
// phase starts mutating the AST with coverage machinery.
FsmDetectVisitor(FsmState& state, AstNetlist* rootp)
: m_state{state} {
iterate(rootp);
}
};
// Lower the completed FSM graphs into the concrete coverage declarations,
// previous-state tracking, and pre/post-triggered instrumentation that the
// runtime uses to record state and transition coverage.
class FsmLowerVisitor final {
// STATE - across all visitors
const FsmState& m_state;
// METHODS
// Rebuild a state-typed constant using the tracked state variable
// width/sign so emitted comparisons match the original representation.
static AstConst* makeStateConst(FileLine* flp, AstVarScope* vscp, int value) {
V3Number num{flp, vscp->width(), static_cast<uint32_t>(value)};
num.isSigned(vscp->dtypep()->isSigned());
return new AstConst{flp, num};
}
// Build guards incrementally without forcing callers to special-case the
// first predicate; this keeps emitted state/arc conditions readable.
static AstNodeExpr* andExpr(FileLine* flp, AstNodeExpr* lhsp, AstNodeExpr* rhsp) {
if (!lhsp) return rhsp;
return new AstLogAnd{flp, lhsp, rhsp};
}
static AstNodeExpr* buildResetCond(FileLine* flp, AstVarScope* resetVscp,
const FsmResetCondDesc&) {
return new AstVarRef{flp, resetVscp, VAccess::READ};
}
// Rebuild the original event control from the saved sense description so
// post-state coverage sampling runs on the same triggering edges.
static AstSenTree* buildSenTree(
FileLine* flp, const std::vector<FsmSenDesc>& senses) {
AstSenTree* const sentreep = new AstSenTree{flp, nullptr};
for (const FsmSenDesc& sense : senses) {
AstSenItem* const senItemp = new AstSenItem{
flp, VEdgeType{sense.edgeType},
new AstVarRef{flp, sense.varScopep, VAccess::READ}};
sentreep->addSensesp(senItemp);
}
return sentreep;
}
// Lower one fully detected FSM graph into the concrete coverage machinery
// used by generated models: declarations, previous-state tracking, and the
// pre/post-triggered increment logic for states and arcs.
void buildOne(const FsmGraph& graph) {
AstAlways* const alwaysp = graph.alwaysp();
AstScope* const scopep = graph.scopep();
AstVarScope* const stateVscp = graph.stateVarScopep();
FileLine* const flp = graph.fileline();
AstNodeModule* const modp = scopep->modp();
AstNodeDType* const prevDTypep
= scopep->findLogicDType(stateVscp->width(), stateVscp->width(),
stateVscp->dtypep()->numeric());
AstVarScope* const prevVscp
= scopep->createTemp("__Vfsmcov_prev__" + stateVscp->varp()->shortName(), prevDTypep);
// The saved previous-state temp crosses the scheduler's pre/post split
// in the same way as Verilator's built-in NBA shadow variables, so keep
// both vars marked as post-life participants for stable MT ordering.
stateVscp->optimizeLifePost(true);
prevVscp->optimizeLifePost(true);
AstActive* const initActivep
= new AstActive{flp, "fsm-coverage-init",
new AstSenTree{flp, new AstSenItem{flp, AstSenItem::Initial{}}}};
initActivep->senTreeStorep(initActivep->sentreep());
// Seed the previous-state temp during initialization so the first
// clock edge compares against a defined state value.
initActivep->addStmtsp(new AstInitialStatic{
flp, new AstAssign{flp, new AstVarRef{flp, prevVscp, VAccess::WRITE},
new AstVarRef{flp, stateVscp, VAccess::READ}}});
scopep->addBlocksp(initActivep);
AstAlwaysPost* const covPostp = new AstAlwaysPost{flp};
// Save the previous state as plain sequential logic at the front of
// the original always_ff body, then evaluate coverage in post logic
// after the delayed state update commits. This avoids a scheduler race
// between a separate AstAlwaysPre task and the real state commit.
AstNode* const bodysp = alwaysp->stmtsp()->unlinkFrBackWithNext();
alwaysp->addStmtsp(new AstAssign{flp, new AstVarRef{flp, prevVscp, VAccess::WRITE},
new AstVarRef{flp, stateVscp, VAccess::READ}});
alwaysp->addStmtsp(bodysp);
for (const V3GraphVertex& vtx : graph.vertices()) {
const FsmVertex* const vertexp = vtx.as<FsmVertex>();
if (!vertexp->isState()) continue;
const FsmStateVertex* const statep = vtx.as<FsmStateVertex>();
// State coverage fires when the FSM enters a state from any other
// value, so repeated self-holds do not count as new entries.
AstCoverOtherDecl* const declp = new AstCoverOtherDecl{
flp, "v_fsm_state/" + modp->prettyName(),
graph.stateVarName() + "::" + statep->label(), "", 0, graph.stateVarName(), "",
statep->label()};
declp->hier(scopep->prettyName());
modp->addStmtsp(declp);
AstNodeExpr* const guardp
= andExpr(flp,
new AstNeq{flp, new AstVarRef{flp, prevVscp, VAccess::READ},
makeStateConst(flp, prevVscp, statep->value())},
new AstEq{flp, new AstVarRef{flp, stateVscp, VAccess::READ},
makeStateConst(flp, stateVscp, statep->value())});
covPostp->addStmtsp(new AstIf{flp, guardp, new AstCoverInc{flp, declp}});
}
for (const V3GraphVertex& vtx : graph.vertices()) {
const FsmVertex* const fromVertexp = vtx.as<FsmVertex>();
for (const V3GraphEdge& edge : fromVertexp->outEdges()) {
const FsmArcEdge* const arcp = edge.as<FsmArcEdge>();
const FsmStateVertex* const toStatep = arcp->top()->as<FsmStateVertex>();
// Arc coverage mirrors the extracted graph exactly, including
// reset and synthetic-default sources, so reports match the
// reviewer-visible graph dump and the user-visible annotation.
const string resetTag
= arcp->isReset() ? (graph.resetInclude() ? "[reset_include]" : "[reset]") : "";
const string fsmTag = arcp->isReset() ? (graph.resetInclude() ? "reset_include"
: "reset")
: arcp->isDefault() ? "default"
: "";
AstCoverOtherDecl* const declp = new AstCoverOtherDecl{
flp, "v_fsm_arc/" + modp->prettyName(),
graph.stateVarName() + "::" + fromVertexp->label() + "->" + toStatep->label()
+ resetTag,
"",
0,
graph.stateVarName(),
fromVertexp->label(),
toStatep->label(),
fsmTag};
declp->hier(scopep->prettyName());
modp->addStmtsp(declp);
AstNodeExpr* guardp = nullptr;
if (fromVertexp->isResetAny()) {
// Reset arcs are modeled as pseudo-source edges in the
// graph, then reconstructed here into the original simple
// reset predicate combined with the destination state.
guardp = buildResetCond(flp, graph.resetCond().varScopep, graph.resetCond());
guardp = andExpr(flp, guardp,
new AstEq{flp, new AstVarRef{flp, stateVscp, VAccess::READ},
makeStateConst(flp, stateVscp, toStatep->value())});
} else if (fromVertexp->isDefaultAny()) {
// Synthetic default arcs mean "none of the explicit
// source states matched", so rebuild that as a conjunction
// of previous-state != known-state tests.
for (const V3GraphVertex& stateVtx : graph.vertices()) {
const FsmVertex* const stateVertexp = stateVtx.as<FsmVertex>();
if (!stateVertexp->isState()) continue;
guardp = andExpr(
flp, guardp,
new AstNeq{flp, new AstVarRef{flp, prevVscp, VAccess::READ},
makeStateConst(flp, prevVscp, stateVertexp->value())});
}
guardp = andExpr(flp, guardp,
new AstEq{flp, new AstVarRef{flp, stateVscp, VAccess::READ},
makeStateConst(flp, stateVscp, toStatep->value())});
} else {
guardp = andExpr(
flp,
new AstEq{flp, new AstVarRef{flp, prevVscp, VAccess::READ},
makeStateConst(flp, prevVscp, fromVertexp->value())},
new AstEq{flp, new AstVarRef{flp, stateVscp, VAccess::READ},
makeStateConst(flp, stateVscp, toStatep->value())});
}
covPostp->addStmtsp(new AstIf{flp, guardp, new AstCoverInc{flp, declp}});
}
}
AstSenTree* const sentreep = buildSenTree(flp, graph.senses());
AstActive* const activep = new AstActive{flp, "fsm-coverage", sentreep};
activep->senTreeStorep(sentreep);
scopep->addBlocksp(activep);
activep->addStmtsp(covPostp);
}
public:
// CONSTRUCTORS
// Lower every detected FSM graph from the shared local state into
// concrete coverage instrumentation while the saved scoped pointers are
// still valid in the same pass.
explicit FsmLowerVisitor(const FsmState& state)
: m_state{state} {
for (const std::pair<const string, DetectedFsm>& it : m_state.fsms()) {
buildOne(*it.second.graphp);
}
}
};
} // namespace
void V3FsmDetect::detect(AstNetlist* rootp) {
UINFO(2, __FUNCTION__ << ":");
FsmState state;
// Phase 1: recover each supported FSM into a complete graph while the
// original clocked/case structure is still easy to recognize.
FsmDetectVisitor detect{state, rootp};
if (dumpGraphLevel() >= 6) {
size_t index = 0;
for (const std::pair<const string, DetectedFsm>& it : state.fsms()) {
it.second.graphp->dumpDotFilePrefixed(it.second.graphp->dumpTag(index++));
}
}
// Phase 2: lower the completed in-memory graph state immediately, without
// crossing into another pass owner or serializing through AST placeholders.
{ FsmLowerVisitor lower{state}; }
V3Global::dumpCheckGlobalTree("fsm-detect", 0, dumpTreeEitherLevel() >= 3);
}

33
src/V3FsmDetect.h Normal file
View File

@ -0,0 +1,33 @@
// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: FSM coverage detect pass
//
// Code available from: https://verilator.org
//
//*************************************************************************
//
// 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
//
//*************************************************************************
#ifndef VERILATOR_V3FSMDETECT_H_
#define VERILATOR_V3FSMDETECT_H_
#include "config_build.h"
#include "verilatedos.h"
class AstNetlist;
class V3FsmDetect final {
public:
// Detect FSMs while the original clocked/case structure is still visible,
// then immediately lower the recovered graphs into concrete coverage
// instrumentation as a second local phase in the same pass.
static void detect(AstNetlist* rootp) VL_MT_DISABLED;
};
#endif

View File

@ -611,6 +611,18 @@ class LinkParseVisitor final : public VNVisitor {
m_varp->attrSplitVar(true);
}
VL_DO_DANGLING(nodep->unlinkFrBack()->deleteTree(), nodep);
} else if (nodep->attrType() == VAttrType::VAR_FSM_ARC_INCLUDE_COND) {
UASSERT_OBJ(m_varp, nodep, "Attribute not attached to variable");
m_varp->attrFsmArcInclCond(true);
VL_DO_DANGLING(nodep->unlinkFrBack()->deleteTree(), nodep);
} else if (nodep->attrType() == VAttrType::VAR_FSM_RESET_ARC) {
UASSERT_OBJ(m_varp, nodep, "Attribute not attached to variable");
m_varp->attrFsmResetArc(true);
VL_DO_DANGLING(nodep->unlinkFrBack()->deleteTree(), nodep);
} else if (nodep->attrType() == VAttrType::VAR_FSM_STATE) {
UASSERT_OBJ(m_varp, nodep, "Attribute not attached to variable");
m_varp->attrFsmState(true);
VL_DO_DANGLING(nodep->unlinkFrBack()->deleteTree(), nodep);
} else if (nodep->attrType() == VAttrType::VAR_SC_BIGUINT) {
UASSERT_OBJ(m_varp, nodep, "Attribute not attached to variable");
m_varp->attrScBigUint(true);

View File

@ -1352,6 +1352,7 @@ void V3Options::parseOptsList(FileLine* fl, const string& optdir, int argc,
DECL_OPTION("-coverage", CbOnOff, [this](bool flag) { coverage(flag); });
DECL_OPTION("-coverage-expr", OnOff, &m_coverageExpr);
DECL_OPTION("-coverage-expr-max", Set, &m_coverageExprMax);
DECL_OPTION("-coverage-fsm", OnOff, &m_coverageFsm);
DECL_OPTION("-coverage-line", OnOff, &m_coverageLine);
DECL_OPTION("-coverage-max-width", Set, &m_coverageMaxWidth);
DECL_OPTION("-coverage-toggle", OnOff, &m_coverageToggle);

View File

@ -226,6 +226,7 @@ private:
bool m_build = false; // main switch: --build
bool m_context = true; // main switch: --Wcontext
bool m_coverageExpr = false; // main switch: --coverage-expr
bool m_coverageFsm = false; // main switch: --coverage-fsm
bool m_coverageLine = false; // main switch: --coverage-block
bool m_coverageToggle = false; // main switch: --coverage-toggle
bool m_coverageUnderscore = false; // main switch: --coverage-underscore
@ -447,7 +448,8 @@ private:
void optimize(int level);
void showVersion(bool verbose);
void coverage(bool flag) {
m_coverageLine = m_coverageToggle = m_coverageExpr = m_coverageUser = flag;
m_coverageLine = m_coverageToggle = m_coverageExpr = m_coverageFsm = m_coverageUser
= flag;
}
static bool suffixed(const string& sw, const char* arg);
static string parseFileArg(const string& optdir, const string& relfilename);
@ -508,9 +510,19 @@ public:
void buildDepBin(const string& flag) { m_buildDepBin = flag; }
bool context() const VL_MT_SAFE { return m_context; }
bool coverage() const VL_MT_SAFE {
// Any enabled coverage kind, including FSM coverage. Code generation
// and runtime support should generally query this accessor.
return m_coverageLine || m_coverageToggle || m_coverageExpr || m_coverageUser
|| m_coverageFsm;
}
bool coverageNonFsm() const VL_MT_SAFE {
// The broad line/toggle/expr/user coverage transforms use this
// accessor. FSM coverage shares the overall coverage umbrella, but its
// extraction still happens through a separate early-recognition path.
return m_coverageLine || m_coverageToggle || m_coverageExpr || m_coverageUser;
}
bool coverageExpr() const { return m_coverageExpr; }
bool coverageFsm() const { return m_coverageFsm; }
bool coverageLine() const { return m_coverageLine; }
bool coverageToggle() const { return m_coverageToggle; }
bool coverageUnderscore() const { return m_coverageUnderscore; }

View File

@ -54,6 +54,7 @@
#include "V3Expand.h"
#include "V3File.h"
#include "V3Force.h"
#include "V3FsmDetect.h"
#include "V3Fork.h"
#include "V3FuncOpt.h"
#include "V3Gate.h"
@ -232,9 +233,11 @@ static void process() {
v3Global.vlExit(0);
}
// Coverage insertion
// Before we do dead code elimination and inlining, or we'll lose it.
if (v3Global.opt.coverage()) V3Coverage::coverage(v3Global.rootp());
// Insert generic non-FSM coverage before dead code elimination and
// inlining, or those opportunities may be optimized away. FSM
// coverage is handled later in V3FsmDetect, after scoping has created
// the AST context needed to recover and lower FSMs reliably.
if (v3Global.opt.coverageNonFsm()) V3Coverage::coverage(v3Global.rootp());
// Resolve randsequence if they are used by the design
if (v3Global.useRandSequence()) V3RandSequence::randSequenceNetlist(v3Global.rootp());
@ -347,6 +350,12 @@ static void process() {
// No more AstAlias after linkDotScope
V3Scope::scopeAll(v3Global.rootp());
V3LinkDot::linkDotScope(v3Global.rootp());
// FSM coverage needs scopes, but should otherwise run as early as
// possible before later lowering rewrites user-visible clocked
// case structure. This entry point runs two adjacent phases:
// detect into local graph state, then lower that completed state
// into the concrete coverage machinery.
if (v3Global.opt.coverageFsm()) V3FsmDetect::detect(v3Global.rootp());
// Relocate classes (after linkDot)
V3Class::classAll(v3Global.rootp());
@ -428,8 +437,9 @@ static void process() {
"This may cause ordering problems.");
}
// Combine COVERINCs with duplicate terms
if (v3Global.opt.coverage()) V3CoverageJoin::coverageJoin(v3Global.rootp());
// Combine generic COVERINCs with duplicate terms. FSM coverage is
// already lowered separately inside V3FsmDetect.
if (v3Global.opt.coverageNonFsm()) V3CoverageJoin::coverageJoin(v3Global.rootp());
// Remove unused vars
V3Const::constifyAll(v3Global.rootp());
@ -447,7 +457,7 @@ static void process() {
}
// Create delayed assignments
// This creates lots of duplicate ACTIVES so ActiveTop needs to be after this step
// This creates lots of duplicate ACTIVES so ActiveTop needs to be after this step.
V3Delayed::delayedAll(v3Global.rootp());
// Make Active's on the top level.

View File

@ -69,6 +69,7 @@ void VlcOptions::parseOptsList(int argc, char** argv) {
DECL_OPTION("-debug", CbCall, []() { V3Error::debugDefault(3); });
DECL_OPTION("-debugi", CbVal, [](int v) { V3Error::debugDefault(v); });
DECL_OPTION("-filter-type", Set, &m_filterType);
DECL_OPTION("-include-reset-arcs", OnOff, &m_includeResetArcs);
DECL_OPTION("-rank", OnOff, &m_rank);
DECL_OPTION("-unlink", OnOff, &m_unlink);
DECL_OPTION("-V", CbCall, []() {

View File

@ -39,6 +39,7 @@ class VlcOptions final {
bool m_annotateAll = false; // main switch: --annotate-all
int m_annotateMin = 10; // main switch: --annotate-min I<count>
bool m_annotatePoints = false; // main switch: --annotate-points
bool m_includeResetArcs = false; // main switch: --include-reset-arcs
string m_filterType = "*"; // main switch: --filter-type
VlStringSet m_readFiles; // main switch: --read
bool m_rank = false; // main switch: --rank
@ -67,6 +68,7 @@ public:
int annotateMin() const { return m_annotateMin; }
bool countOk(uint64_t count) const { return count >= static_cast<uint64_t>(m_annotateMin); }
bool annotatePoints() const { return m_annotatePoints; }
bool includeResetArcs() const { return m_includeResetArcs; }
bool rank() const { return m_rank; }
bool unlink() const { return m_unlink; }
string writeFile() const { return m_writeFile; }

View File

@ -70,6 +70,18 @@ public:
return keyExtract(VL_CIK_THRESH, m_name.c_str());
}
string linescov() const { return keyExtract(VL_CIK_LINESCOV, m_name.c_str()); }
bool isFsmState() const { return type() == "fsm_state"; }
bool isFsmArc() const { return type() == "fsm_arc"; }
// Arc-specific helpers are used after callers have already filtered to
// FSM arc points, so they do not repeat the type check here.
string fsmVarName() const { return keyExtract(VL_CIK_FSM_VAR, m_name.c_str()); }
string fsmFromState() const { return keyExtract(VL_CIK_FSM_FROM, m_name.c_str()); }
string fsmToState() const { return keyExtract(VL_CIK_FSM_TO, m_name.c_str()); }
string fsmTag() const { return keyExtract(VL_CIK_FSM_TAG, m_name.c_str()); }
bool isFsmResetInclude() const { return fsmTag() == "reset_include"; }
bool isFsmResetArc() const { return fsmTag() == "reset"; }
bool isFsmDefaultArc() const { return fsmTag() == "default"; }
bool fsmIsReset() const { return isFsmResetArc() || isFsmResetInclude(); }
int lineno() const {
const string lineStr = keyExtract(VL_CIK_LINENO, m_name.c_str());
return std::atoi(lineStr.c_str());

View File

@ -25,6 +25,8 @@
#include <algorithm>
#include <fstream>
#include <map>
#include <set>
#include <string>
#include <vector>
@ -122,12 +124,18 @@ void VlcTop::writeInfo(const string& filename) {
int branchesHit = 0;
for (auto& li : lines) {
VlcSourceCount& sc = li.second;
os << "DA:" << sc.lineno() << "," << sc.maxCount() << "\n";
const int num_branches = sc.points().size();
if (num_branches == 1) continue;
branchesFound += num_branches;
int point_num = 0;
uint64_t daCount = 0;
std::vector<const VlcPoint*> infoPoints;
for (const auto& point : sc.points()) {
if (point->isFsmArc()) continue;
daCount = std::max(daCount, point->count());
if (!point->isFsmState()) infoPoints.push_back(point);
}
os << "DA:" << sc.lineno() << "," << daCount << "\n";
if (infoPoints.size() <= 1) continue;
branchesFound += static_cast<int>(infoPoints.size());
int point_num = 0;
for (const VlcPoint* point : infoPoints) {
os << "BRDA:" << sc.lineno() << ",";
os << "0,";
os << point_num << ",";
@ -328,6 +336,29 @@ void VlcTop::annotateOutputFiles(const string& dirname) {
if (opt.annotatePoints()) {
for (const auto& pit : sc.points()) pit->dumpAnnotate(os, opt.annotateMin());
}
bool printedFsmHeader = false;
for (const auto& pit : sc.points()) {
if (!pit->isFsmState() && !pit->isFsmArc()) continue;
if (!printedFsmHeader) {
os << " // [FSM coverage]\n";
printedFsmHeader = true;
}
os << (opt.countOk(pit->count()) ? " " : "%");
os << std::setfill('0') << std::setw(6) << pit->count() << " ";
if (pit->isFsmState()) {
os << "// [fsm_state " << pit->comment() << "]";
if (pit->count() == 0) os << " *** UNCOVERED ***";
os << "\n";
} else if (pit->isFsmDefaultArc()) {
os << "// [SYNTHETIC DEFAULT ARC: " << pit->comment() << "]\n";
} else {
os << "// [fsm_arc " << pit->comment() << "]";
if (pit->fsmIsReset() && !opt.includeResetArcs()) {
os << " [reset arc, excluded from %]";
}
os << "\n";
}
}
}
}
}

View File

@ -848,6 +848,14 @@ vnum {vnum1}|{vnum2}|{vnum3}|{vnum4}|{vnum5}
"/*verilator sc_clock*/" { FL; yylval.fl->v3warn(DEPRECATED, "sc_clock is ignored"); FL_BRK; }
"/*verilator sformat*/" { FL; return yVL_SFORMAT; }
"/*verilator split_var*/" { FL; return yVL_SPLIT_VAR; }
/* Experimental Verilator-specific FSM coverage controls. These names were
* chosen to match the current extractor behavior, not a published synthesis
* or simulator pragma standard, so they may evolve as we settle on longer-
* term compatibility/aliasing.
*/
"/*verilator fsm_arc_include_cond*/" { FL; return yVL_FSM_ARC_INCL_COND; }
"/*verilator fsm_reset_arc*/" { FL; return yVL_FSM_RESET_ARC; }
"/*verilator fsm_state*/" { FL; return yVL_FSM_STATE; }
"/*verilator tag"[^*]*"*/" { FL; yylval.strp = PARSEP->newString(V3ParseImp::lexParseTag(yytext));
return yVL_TAG; }
"/*verilator timing_off*/" { FL_FWD; PARSEP->lexFileline()->timingOn(false); FL_BRK; }

View File

@ -804,6 +804,9 @@ BISONPRE_VERSION(3.7,%define api.header.include {"V3ParseBison.h"})
%token<fl> yVL_SC_BV "/*verilator sc_bv*/"
%token<fl> yVL_SFORMAT "/*verilator sformat*/"
%token<fl> yVL_SPLIT_VAR "/*verilator split_var*/"
%token<fl> yVL_FSM_ARC_INCL_COND "/*verilator fsm_arc_include_cond*/"
%token<fl> yVL_FSM_RESET_ARC "/*verilator fsm_reset_arc*/"
%token<fl> yVL_FSM_STATE "/*verilator fsm_state*/"
%token<strp> yVL_TAG "/*verilator tag*/"
%token<fl> yVL_UNROLL_DISABLE "/*verilator unroll_disable*/"
%token<fl> yVL_UNROLL_FULL "/*verilator unroll_full*/"
@ -3123,6 +3126,9 @@ sigAttr<nodep>:
| yVL_SC_BV { $$ = new AstAttrOf{$1, VAttrType::VAR_SC_BV}; }
| yVL_SFORMAT { $$ = new AstAttrOf{$1, VAttrType::VAR_SFORMAT}; }
| yVL_SPLIT_VAR { $$ = new AstAttrOf{$1, VAttrType::VAR_SPLIT_VAR}; }
| yVL_FSM_ARC_INCL_COND { $$ = new AstAttrOf{$1, VAttrType::VAR_FSM_ARC_INCLUDE_COND}; }
| yVL_FSM_RESET_ARC { $$ = new AstAttrOf{$1, VAttrType::VAR_FSM_RESET_ARC}; }
| yVL_FSM_STATE { $$ = new AstAttrOf{$1, VAttrType::VAR_FSM_STATE}; }
;
rangeListE<nodeRangep>: // IEEE: [{packed_dimension}]

View File

@ -0,0 +1,65 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM coverage basic test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input clk
);
typedef enum logic [1:0] {
S_IDLE = 2'd0,
S_RUN = 2'd1,
S_DONE = 2'd2,
S_ERR = 2'd3
} state_t;
logic rst;
logic start;
integer cyc;
state_t state /*verilator fsm_reset_arc*/;
initial begin
rst = 1'b1;
start = 1'b0;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) rst <= 1'b0;
if (cyc == 2) start <= 1'b1;
if (cyc == 3) start <= 1'b0;
if (cyc == 8) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
always_ff @(posedge clk) begin
if (rst) begin
state <= S_IDLE;
end else begin
%000004 case (state)
// [FSM coverage]
%000001 // [fsm_arc t.state::ANY->S_IDLE[reset_include]] [reset arc, excluded from %]
%000004 // [fsm_arc t.state::S_DONE->S_DONE]
%000003 // [fsm_arc t.state::S_IDLE->S_IDLE]
%000001 // [fsm_arc t.state::S_IDLE->S_RUN]
%000001 // [fsm_arc t.state::S_RUN->S_DONE]
%000001 // [fsm_state t.state::S_DONE]
%000000 // [fsm_state t.state::S_ERR] *** UNCOVERED ***
%000000 // [fsm_state t.state::S_IDLE] *** UNCOVERED ***
%000001 // [fsm_state t.state::S_RUN]
S_IDLE: if (start) state <= S_RUN; else state <= S_IDLE;
S_RUN: state <= S_DONE;
S_DONE: state <= S_DONE;
default: state <= S_ERR;
endcase
end
end
endmodule

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage basic test
#
# 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 os
import vltest_bootstrap
test.scenarios('simulator')
test.compile(verilator_flags2=['--cc --coverage-fsm'])
test.execute()
test.run(cmd=[
os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
"--annotate",
test.obj_dir + "/annotated",
test.obj_dir + "/coverage.dat",
],
verilator_run=True)
test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename)
test.passes()

View File

@ -0,0 +1,53 @@
// DESCRIPTION: Verilator: FSM coverage basic test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input clk
);
typedef enum logic [1:0] {
S_IDLE = 2'd0,
S_RUN = 2'd1,
S_DONE = 2'd2,
S_ERR = 2'd3
} state_t;
logic rst;
logic start;
integer cyc;
state_t state /*verilator fsm_reset_arc*/;
initial begin
rst = 1'b1;
start = 1'b0;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) rst <= 1'b0;
if (cyc == 2) start <= 1'b1;
if (cyc == 3) start <= 1'b0;
if (cyc == 8) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
always_ff @(posedge clk) begin
if (rst) begin
state <= S_IDLE;
end else begin
case (state)
S_IDLE: if (start) state <= S_RUN; else state <= S_IDLE;
S_RUN: state <= S_DONE;
S_DONE: state <= S_DONE;
default: state <= S_ERR;
endcase
end
end
endmodule

View File

@ -0,0 +1,63 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM coverage begin-wrapped/if-else test
//
// 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
module t(
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
logic rst;
logic sel;
int cyc;
state_t state /*verilator fsm_reset_arc*/;
initial begin
rst = 1'b1;
sel = 1'b0;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) rst <= 1'b0;
if (cyc == 2) sel <= 1'b1;
if (cyc == 3) sel <= 1'b0;
if (cyc == 6) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
always_ff @(posedge clk) begin
if (rst) begin
state <= S0;
end else begin
%000003 case (state)
// [FSM coverage]
%000001 // [fsm_arc t.state::ANY->S0[reset_include]] [reset arc, excluded from %]
%000000 // [fsm_arc t.state::S0->S1]
%000003 // [fsm_arc t.state::S0->S2]
%000000 // [fsm_arc t.state::S1->S0]
%000002 // [fsm_state t.state::S0]
%000000 // [fsm_state t.state::S1] *** UNCOVERED ***
%000003 // [fsm_state t.state::S2]
S0: if (sel) state <= S1; else state <= S2;
S1: state <= S0;
default: state <= S0;
endcase
end
end
endmodule

View File

@ -0,0 +1,31 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage begin-wrapped/if-else extraction test
#
# 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 os
import vltest_bootstrap
test.scenarios('simulator')
test.compile(verilator_flags2=['--cc --coverage-fsm'])
test.execute()
# Use annotated-source output so the expected file captures both the extracted
# FSM shape and the per-point hit counts.
test.run(cmd=[
os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
"--annotate",
test.obj_dir + "/annotated",
test.obj_dir + "/coverage.dat",
],
verilator_run=True)
test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename)
test.passes()

View File

@ -0,0 +1,53 @@
// DESCRIPTION: Verilator: FSM coverage begin-wrapped/if-else test
//
// 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
module t(
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
logic rst;
logic sel;
int cyc;
state_t state /*verilator fsm_reset_arc*/;
initial begin
rst = 1'b1;
sel = 1'b0;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) rst <= 1'b0;
if (cyc == 2) sel <= 1'b1;
if (cyc == 3) sel <= 1'b0;
if (cyc == 6) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
always_ff @(posedge clk) begin
if (rst) begin
state <= S0;
end else begin
case (state)
S0: if (sel) state <= S1; else state <= S2;
S1: state <= S0;
default: state <= S0;
endcase
end
end
endmodule

View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM lowered coverage declaration dump test
#
# 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
from pathlib import Path
import vltest_bootstrap
test.scenarios('vlt')
test.top_filename = "t/t_cover_fsm_styles.v"
# Dump the lowered AST so AstCoverOtherDecl::dump() sees FSM metadata-bearing
# coverage declarations directly. This avoids JSON/schema coupling while still
# covering the dump-side formatting for fv/ff/ft/fg.
test.lint(v_flags=["--coverage-fsm", "--dump-tree"])
tree_files = [Path(filename) for filename in test.glob_some(test.obj_dir + "/*.tree")]
tree_texts = [filename.read_text(encoding="utf8") for filename in tree_files]
assert any("COVEROTHERDECL" in text and " fv=t.state" in text for text in tree_texts)
assert any(
"COVEROTHERDECL" in text and " ff=ANY" in text and " ft=S0" in text and " fg=reset" in text
for text in tree_texts
)
assert any(
"COVEROTHERDECL" in text and " ff=default" in text and " ft=S0" in text and " fg=default"
in text
for text in tree_texts
)
test.passes()

View File

@ -0,0 +1,6 @@
%Warning-COVERIGN: t/t_cover_fsm_enum_bad.v:27:19: Ignoring unsupported: FSM coverage on enum state transitions that assign a constant not present in the declared enum
27 | S0: state <= 2'd3;
| ^~
... For warning description see https://verilator.org/warn/COVERIGN?v=latest
... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message.
%Error: Exiting due to

View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM enum transition bad-value test
#
# 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('vlt')
# When an enum-backed FSM assigns a constant that is not one of the declared
# enum items, FSM coverage should warn and skip the unsupported edge rather
# than turning optional coverage into a hard compile failure.
test.lint(
verilator_flags2=["--coverage-fsm"],
fails=True)
test.file_grep(
test.compile_log_filename,
r'%Warning-COVERIGN: t/t_cover_fsm_enum_bad.v:27:19: Ignoring unsupported: FSM coverage '
r'on enum state transitions that assign a constant not present in the declared enum')
test.passes()

View File

@ -0,0 +1,34 @@
// DESCRIPTION: Verilator: FSM enum transition rejects unknown constant values
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input logic clk,
input logic rst
);
typedef enum logic [1:0] {
S0, S1
} state_t;
state_t state;
// FSM coverage should reject a constant next-state value that is not one of
// the declared enum items. This keeps graph construction aligned with the
// enum-backed state set instead of silently dropping the transition.
always_ff @(posedge clk) begin
if (rst) begin
state <= S0;
end else begin
case (state)
/* verilator lint_off ENUMVALUE */
S0: state <= 2'd3;
/* verilator lint_on ENUMVALUE */
default: state <= S0;
endcase
end
end
endmodule

View File

@ -0,0 +1,6 @@
%Warning-COVERIGN: t/t_cover_fsm_enumwide_bad.v:25:7: Ignoring unsupported: FSM coverage on enum-typed state variables wider than 32 bits
25 | case (state)
| ^~~~
... For warning description see https://verilator.org/warn/COVERIGN?v=latest
... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message.
%Error: Exiting due to

View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM enum width limit test
#
# 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('vlt')
# FSM coverage currently stores recovered enum state values in the detector's
# 32-bit internal representation, so wider enum-backed FSMs are rejected.
test.lint(
verilator_flags2=["--coverage-fsm"],
fails=True)
test.file_grep(
test.compile_log_filename,
r'%Warning-COVERIGN: t/t_cover_fsm_enumwide_bad.v:25:7: Ignoring unsupported: '
r'FSM coverage on enum-typed state variables wider than 32 bits')
test.passes()

View File

@ -0,0 +1,32 @@
// DESCRIPTION: Verilator: FSM enum width limit rejects >32-bit enums
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input logic clk,
input logic rst
);
typedef enum logic [32:0] {
S0 = 33'd0,
S1 = 33'd1
} state_t;
state_t state;
// FSM coverage currently supports enum-backed state variables only up to
// 32 bits wide, so this wider enum should be rejected at FSM detection time.
always_ff @(posedge clk) begin
if (rst) begin
state <= S0;
end else begin
case (state)
S0: state <= S1;
default: state <= S0;
endcase
end
end
endmodule

View File

@ -0,0 +1,21 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage stays off without --coverage-fsm
#
# 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=['--cc --coverage-line'])
test.execute()
test.file_grep_not(test.obj_dir + "/coverage.dat", r"fsm_state")
test.file_grep_not(test.obj_dir + "/coverage.dat", r"fsm_arc")
test.passes()

View File

@ -0,0 +1,53 @@
// DESCRIPTION: Verilator: FSM coverage stays off without --coverage-fsm
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input clk
);
typedef enum logic [1:0] {
S_IDLE = 2'd0,
S_RUN = 2'd1,
S_DONE = 2'd2,
S_ERR = 2'd3
} state_t;
logic rst;
logic start;
integer cyc;
state_t state /*verilator fsm_reset_arc*/;
initial begin
rst = 1'b1;
start = 1'b0;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) rst <= 1'b0;
if (cyc == 2) start <= 1'b1;
if (cyc == 3) start <= 1'b0;
if (cyc == 8) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
always_ff @(posedge clk) begin
if (rst) begin
state <= S_IDLE;
end else begin
case (state)
S_IDLE: if (start) state <= S_RUN; else state <= S_IDLE;
S_RUN: state <= S_DONE;
S_DONE: state <= S_DONE;
default: state <= S_ERR;
endcase
end
end
endmodule

View File

@ -0,0 +1,51 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM coverage forced non-enum test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input clk
);
integer cyc;
logic rst;
logic [1:0] state /*verilator fsm_state*/;
initial begin
cyc = 0;
rst = 1'b1;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) rst <= 1'b0;
if (cyc == 6) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
always_ff @(posedge clk) begin
if (rst) begin
state <= 2'd0;
end else begin
%000002 case (state)
// [FSM coverage]
%000001 // [fsm_arc t.state::ANY->S0[reset]] [reset arc, excluded from %]
%000002 // [fsm_arc t.state::S0->S1]
%000002 // [fsm_arc t.state::S1->S2]
%000001 // [fsm_state t.state::S0]
%000002 // [fsm_state t.state::S1]
%000002 // [fsm_state t.state::S2]
%000000 // [fsm_state t.state::S3] *** UNCOVERED ***
2'd0: state <= 2'd1;
2'd1: state <= 2'd2;
default: state <= 2'd0;
endcase
end
end
endmodule

View File

@ -0,0 +1,32 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage forced non-enum test
#
# 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 os
import vltest_bootstrap
test.scenarios('simulator')
test.compile(verilator_flags2=['--cc --coverage-fsm'])
test.execute()
# Use annotated-source golden output so hit-count regressions are visible in the
# expected file instead of being hidden behind coarse coverage.dat greps.
test.run(cmd=[
os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
"--annotate",
test.obj_dir + "/annotated",
test.obj_dir + "/coverage.dat",
],
verilator_run=True)
test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename)
test.passes()

View File

@ -0,0 +1,41 @@
// DESCRIPTION: Verilator: FSM coverage forced non-enum test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input clk
);
integer cyc;
logic rst;
logic [1:0] state /*verilator fsm_state*/;
initial begin
cyc = 0;
rst = 1'b1;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) rst <= 1'b0;
if (cyc == 6) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
always_ff @(posedge clk) begin
if (rst) begin
state <= 2'd0;
end else begin
case (state)
2'd0: state <= 2'd1;
2'd1: state <= 2'd2;
default: state <= 2'd0;
endcase
end
end
endmodule

View File

@ -0,0 +1,24 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage graph dump test
#
# 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('vltmt')
test.top_filename = "t/t_cover_fsm_styles.v"
test.compile(v_flags2=["--coverage-fsm", "--dumpi-graph", "6"], threads=2)
dot_files = test.glob_some(test.obj_dir + "/*fsm_*.dot")
for dot_filename in dot_files:
test.file_grep(dot_filename, r'digraph v3graph')
test.file_grep_any(dot_files, r'ANY')
test.file_grep_any(dot_files, r'default')
test.passes()

View File

@ -0,0 +1,64 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM coverage negative extraction test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
int cyc;
logic side;
state_t state /*verilator fsm_reset_arc*/;
initial begin
cyc = 0;
side = 1'b0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) side <= 1'b1;
if (cyc == 5) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
// The S0 arm is the supported baseline. The S1 and default arms are
// deliberately unsupported extractor shapes: one has two meaningful
// statements, the other writes a different lhs first. Coverage should ignore
// those arcs rather than guessing.
always_ff @(posedge clk) begin
if (cyc == 0) begin
state <= S0;
end else begin
%000002 case (state)
// [FSM coverage]
%000002 // [fsm_arc t.state::S0->S1]
%000001 // [fsm_state t.state::S0]
%000002 // [fsm_state t.state::S1]
%000002 // [fsm_state t.state::S2]
S0: state <= S1;
S1: begin
side <= ~side;
state <= S2;
end
default: begin
side <= 1'b0;
state <= S0;
end
endcase
end
end
endmodule

View File

@ -0,0 +1,37 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage negative extraction test
#
# 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 os
import vltest_bootstrap
test.scenarios('simulator')
# This test is intentionally "half supported": one case item is a simple
# direct state assignment, while the others use shapes the extractor should
# ignore (multiple meaningful statements or assignment to a non-state lhs).
# That lets us hit the conservative negative branches in directStateAssign()
# and singleMeaningfulStmt() without changing user-visible behavior.
test.compile(verilator_flags2=['--cc --coverage-fsm'])
test.execute()
# Use annotated-source output so the golden locks down which candidate arcs
# survive extraction and which unsupported shapes are intentionally skipped.
test.run(cmd=[
os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
"--annotate",
test.obj_dir + "/annotated",
test.obj_dir + "/coverage.dat",
],
verilator_run=True)
test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename)
test.passes()

View File

@ -0,0 +1,57 @@
// DESCRIPTION: Verilator: FSM coverage negative extraction test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
int cyc;
logic side;
state_t state /*verilator fsm_reset_arc*/;
initial begin
cyc = 0;
side = 1'b0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) side <= 1'b1;
if (cyc == 5) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
// The S0 arm is the supported baseline. The S1 and default arms are
// deliberately unsupported extractor shapes: one has two meaningful
// statements, the other writes a different lhs first. Coverage should ignore
// those arcs rather than guessing.
always_ff @(posedge clk) begin
if (cyc == 0) begin
state <= S0;
end else begin
case (state)
S0: state <= S1;
S1: begin
side <= ~side;
state <= S2;
end
default: begin
side <= 1'b0;
state <= S0;
end
endcase
end
end
endmodule

View File

@ -0,0 +1,47 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM coverage no-reset lowering test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input logic clk
);
typedef enum logic [0:0] {
S0 = 1'b0,
S1 = 1'b1
} state_t;
int cyc;
state_t state;
initial begin
cyc = 0;
state = S0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 4) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
// No reset branch on purpose: this keeps the test focused on the branch in
// lowering that skips reset reconstruction entirely.
always_ff @(posedge clk) begin
%000003 case (state)
// [FSM coverage]
%000003 // [fsm_arc t.state::S0->S1]
%000002 // [fsm_state t.state::S0]
%000003 // [fsm_state t.state::S1]
S0: state <= S1;
default: state <= S0;
endcase
end
endmodule

View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage no-reset lowering test
#
# 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 os
import vltest_bootstrap
test.scenarios('simulator')
# This test deliberately uses a clocked FSM with no outer reset branch. It
# keeps coverage extraction in the supported subset, but forces lowering down
# the "hasResetCond() == false" path so we validate the no-reset machinery
# rather than only reset-wrapped FSMs.
test.compile(verilator_flags2=['--cc --coverage-fsm'])
test.execute()
# Use annotated-source output so the expected file captures the no-reset shape
# directly, including the absence of reset pseudo-arcs.
test.run(cmd=[
os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
"--annotate",
test.obj_dir + "/annotated",
test.obj_dir + "/coverage.dat",
],
verilator_run=True)
test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename)
test.passes()

View File

@ -0,0 +1,41 @@
// DESCRIPTION: Verilator: FSM coverage no-reset lowering test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input logic clk
);
typedef enum logic [0:0] {
S0 = 1'b0,
S1 = 1'b1
} state_t;
int cyc;
state_t state;
initial begin
cyc = 0;
state = S0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 4) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
// No reset branch on purpose: this keeps the test focused on the branch in
// lowering that skips reset reconstruction entirely.
always_ff @(posedge clk) begin
case (state)
S0: state <= S1;
default: state <= S0;
endcase
end
endmodule

View File

@ -0,0 +1,63 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM coverage reset policy test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
%000006 input clk
);
typedef enum logic [0:0] {
S0 = 1'b0,
S1 = 1'b1
} state_t;
%000001 logic rst;
integer cyc;
%000001 state_t state_incl /*verilator fsm_reset_arc*/;
%000001 state_t state_excl;
%000001 initial begin
%000001 rst = 1'b1;
%000001 cyc = 0;
end
%000006 always @(posedge clk) begin
%000006 cyc <= cyc + 1;
%000005 if (cyc == 1) rst <= 1'b0;
%000005 if (cyc == 5) begin
%000001 $write("*-* All Finished *-*\n");
%000001 $finish;
end
end
%000006 always_ff @(posedge clk) begin
%000004 if (rst) state_incl <= S0;
%000004 else case (state_incl)
// [FSM coverage]
%000001 // [fsm_arc t.state_incl::ANY->S0[reset_include]] [reset arc, excluded from %]
%000001 // [fsm_arc t.state_incl::S0->S1]
%000000 // [fsm_state t.state_incl::S0] *** UNCOVERED ***
%000001 // [fsm_state t.state_incl::S1]
%000001 S0: state_incl <= S1;
%000003 default: state_incl <= S1;
endcase
end
%000006 always_ff @(posedge clk) begin
%000004 if (rst) state_excl <= S0;
%000004 else case (state_excl)
// [FSM coverage]
%000001 // [fsm_arc t.state_excl::ANY->S0[reset]] [reset arc, excluded from %]
%000001 // [fsm_arc t.state_excl::S0->S1]
%000000 // [fsm_state t.state_excl::S0] *** UNCOVERED ***
%000001 // [fsm_state t.state_excl::S1]
%000001 S0: state_excl <= S1;
%000003 default: state_excl <= S1;
endcase
end
endmodule

View File

@ -0,0 +1,37 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage reset policy test
#
# 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 os
import vltest_bootstrap
test.scenarios('simulator')
test.compile(verilator_flags2=['--cc --coverage'])
test.execute()
test.run(cmd=[
os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
"--include-reset-arcs",
test.obj_dir + "/coverage.dat",
],
verilator_run=True)
test.run(cmd=[
os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
"--annotate",
test.obj_dir + "/annotated",
test.obj_dir + "/coverage.dat",
],
verilator_run=True)
test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename)
test.passes()

View File

@ -0,0 +1,51 @@
// DESCRIPTION: Verilator: FSM coverage reset policy test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input clk
);
typedef enum logic [0:0] {
S0 = 1'b0,
S1 = 1'b1
} state_t;
logic rst;
integer cyc;
state_t state_incl /*verilator fsm_reset_arc*/;
state_t state_excl;
initial begin
rst = 1'b1;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) rst <= 1'b0;
if (cyc == 5) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
always_ff @(posedge clk) begin
if (rst) state_incl <= S0;
else case (state_incl)
S0: state_incl <= S1;
default: state_incl <= S1;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_excl <= S0;
else case (state_excl)
S0: state_excl <= S1;
default: state_excl <= S1;
endcase
end
endmodule

View File

@ -0,0 +1,62 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM coverage reset pseudo-vertex reuse test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
logic rst;
integer cyc;
state_t state /*verilator fsm_reset_arc*/;
initial begin
rst = 1'b1;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) rst <= 1'b0;
if (cyc == 5) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
// This reset block is intentionally non-idiomatic. The detector only collects
// reset arcs from top-level direct assignments in the reset branch, so two
// sequential assignments are the narrowest way to force multiple reset arcs
// into one FSM graph and exercise reuse of the synthetic ANY reset source.
always_ff @(posedge clk) begin
if (rst) begin
state <= S0;
state <= S1;
end else begin
%000001 case (state)
// [FSM coverage]
%000000 // [fsm_arc t.state::ANY->S0[reset_include]] [reset arc, excluded from %]
%000001 // [fsm_arc t.state::ANY->S1[reset_include]] [reset arc, excluded from %]
%000000 // [fsm_arc t.state::S0->S2]
%000001 // [fsm_arc t.state::S1->S2]
%000000 // [fsm_state t.state::S0] *** UNCOVERED ***
%000001 // [fsm_state t.state::S1]
%000001 // [fsm_state t.state::S2]
S0: state <= S2;
S1: state <= S2;
default: state <= S2;
endcase
end
end
endmodule

View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage reset pseudo-vertex reuse test
#
# 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 os
import vltest_bootstrap
test.scenarios('simulator')
# This regression is aimed at the graph helper, not at recommending RTL style.
# We deliberately create two reset arcs in a single FSM so graph construction
# has to reuse the synthetic ANY reset pseudo-vertex rather than allocating it
# only once for a one-arc machine.
test.compile(verilator_flags2=['--cc --coverage-fsm'])
test.execute()
# Use annotated-source output so the golden proves both reset arcs remain
# visible and share the same synthetic ANY reset source.
test.run(cmd=[
os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
"--annotate",
test.obj_dir + "/annotated",
test.obj_dir + "/coverage.dat",
],
verilator_run=True)
test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename)
test.passes()

View File

@ -0,0 +1,52 @@
// DESCRIPTION: Verilator: FSM coverage reset pseudo-vertex reuse test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
logic rst;
integer cyc;
state_t state /*verilator fsm_reset_arc*/;
initial begin
rst = 1'b1;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) rst <= 1'b0;
if (cyc == 5) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
// This reset block is intentionally non-idiomatic. The detector only collects
// reset arcs from top-level direct assignments in the reset branch, so two
// sequential assignments are the narrowest way to force multiple reset arcs
// into one FSM graph and exercise reuse of the synthetic ANY reset source.
always_ff @(posedge clk) begin
if (rst) begin
state <= S0;
state <= S1;
end else begin
case (state)
S0: state <= S2;
S1: state <= S2;
default: state <= S2;
endcase
end
end
endmodule

View File

@ -0,0 +1,64 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM coverage style coverage test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2,
S3 = 2'd3
} state_t;
integer cyc;
logic rst;
logic start;
state_t state /*verilator fsm_arc_include_cond*/;
initial begin
rst = 1'b1;
start = 1'b0;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) rst <= 1'b0;
if (cyc == 2) start <= 1'b1;
if (cyc == 3) start <= 1'b0;
if (cyc == 6) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
always_ff @(posedge clk) begin
if (rst) begin
state <= S0;
end else begin
%000003 case (state)
// [FSM coverage]
%000001 // [fsm_arc t.state::ANY->S0[reset]] [reset arc, excluded from %]
%000000 // [fsm_arc t.state::S0->S1]
%000003 // [fsm_arc t.state::S0->S2]
%000000 // [fsm_arc t.state::S1->S3]
%000000 // [SYNTHETIC DEFAULT ARC: t.state::default->S0]
%000002 // [fsm_state t.state::S0]
%000000 // [fsm_state t.state::S1] *** UNCOVERED ***
%000003 // [fsm_state t.state::S2]
%000000 // [fsm_state t.state::S3] *** UNCOVERED ***
S0: if (start) state <= S1; else state <= S2;
S1: state <= S3;
default: state <= S0;
endcase
end
end
endmodule

View File

@ -0,0 +1,27 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage style coverage test
#
# 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 os
import vltest_bootstrap
test.scenarios('simulator')
test.compile(verilator_flags2=['--cc --coverage-fsm'])
test.execute()
test.run(cmd=[os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
"--annotate", test.obj_dir + "/annotated",
test.obj_dir + "/coverage.dat"],
verilator_run=True) # yapf:disable
test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename)
test.passes()

View File

@ -0,0 +1,52 @@
// DESCRIPTION: Verilator: FSM coverage style coverage test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2,
S3 = 2'd3
} state_t;
integer cyc;
logic rst;
logic start;
state_t state /*verilator fsm_arc_include_cond*/;
initial begin
rst = 1'b1;
start = 1'b0;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) rst <= 1'b0;
if (cyc == 2) start <= 1'b1;
if (cyc == 3) start <= 1'b0;
if (cyc == 6) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
always_ff @(posedge clk) begin
if (rst) begin
state <= S0;
end else begin
case (state)
S0: if (start) state <= S1; else state <= S2;
S1: state <= S3;
default: state <= S0;
endcase
end
end
endmodule

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: generic coverage declaration dump test
#
# 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
from pathlib import Path
import vltest_bootstrap
test.scenarios('vlt')
test.top_filename = "t/t_cover_fsm_styles.v"
# Dump generic COVEROTHERDECL nodes so AstCoverOtherDecl::dump() also sees
# coverage declarations with no FSM metadata, exercising the empty-field side
# of the fv/ff/ft/fg formatting.
test.lint(v_flags=["--coverage-line", "--dump-tree"])
tree_files = [Path(filename) for filename in test.glob_some(test.obj_dir + "/*.tree")]
tree_texts = [filename.read_text(encoding="utf8") for filename in tree_files]
generic_lines = []
for text in tree_texts:
generic_lines.extend(
line for line in text.splitlines() if "COVEROTHERDECL" in line and " page=v_line/" in line
)
assert generic_lines
assert any(" fv=" not in line and " ff=" not in line and " ft=" not in line and " fg=" not in line
for line in generic_lines)
test.passes()

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM metacomment dump test
#
# 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 json
import vltest_bootstrap
test.scenarios('vlt')
test.top_filename = "t/t_fsm_metacmt_dump.v"
test.lint(v_flags=["--dump-tree --dump-tree-json --no-json-edit-nums"])
tree_files = test.glob_some(test.obj_dir + "/*.tree")
json_files = test.glob_some(test.obj_dir + "/*.tree.json")
test.file_grep_any(tree_files, r'\[aFSMSTATE\]')
test.file_grep_any(tree_files, r'\[aFSMRESETARC\]')
test.file_grep_any(tree_files, r'\[aFSMARCCOND\]')
test.file_grep_any(json_files, r'"attrFsmState":true')
test.file_grep_any(json_files, r'"attrFsmResetArc":true')
test.file_grep_any(json_files, r'"attrFsmArcInclCond":true')
for filename in json_files:
with open(filename, 'r', encoding="utf8") as fh:
json.load(fh)
test.passes()

View File

@ -0,0 +1,34 @@
// DESCRIPTION: Verilator: FSM metacomment dump test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input logic clk,
input logic rst
);
typedef enum logic [0:0] {
S0 = 1'b0,
S1 = 1'b1
} state_t;
state_t state_reset /*verilator fsm_reset_arc*/;
state_t state_cond /*verilator fsm_arc_include_cond*/;
logic forced_state /*verilator fsm_state*/;
always_ff @(posedge clk) begin
if (rst) begin
state_reset <= S0;
state_cond <= S0;
forced_state <= 1'b0;
end else begin
state_reset <= S1;
if (state_cond) state_cond <= S0;
else state_cond <= S1;
forced_state <= ~forced_state;
end
end
endmodule

View File

@ -0,0 +1,6 @@
%Warning-COVERIGN: t/t_fsmmulti_same_bad.v:30:5: Ignoring unsupported: FSM coverage on multiple supported case statements found in the same always block. Only the first candidate will be instrumented.
30 | case (state)
| ^~~~
... For warning description see https://verilator.org/warn/COVERIGN?v=latest
... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message.
%Error: Exiting due to

View File

@ -0,0 +1,28 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: same-state multi-candidate FSM error test
#
# 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('vlt')
# Multiple supported case candidates on the same state variable in one
# always_ff now warn and keep only the first candidate instrumented. Different-
# state multi-candidate cases still use the existing FSMMULTI warning path; this
# test locks down only the same-state unsupported form.
test.lint(
verilator_flags2=["--coverage-fsm"],
fails=True)
test.file_grep(
test.compile_log_filename,
r'%Warning-COVERIGN: t/t_fsmmulti_same_bad.v:30:5: Ignoring unsupported: FSM coverage on '
r'multiple supported case statements found in the same always block. Only the first '
r'candidate will be instrumented.')
test.passes()

View File

@ -0,0 +1,36 @@
// DESCRIPTION: Verilator: same-state multi-candidate FSM error test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input logic clk,
input logic rst
);
typedef enum logic [1:0] {
S0, S1, S2
} state_t;
state_t state;
// This is intentionally non-idiomatic RTL. The detector sees one supported
// candidate in the reset-if else branch and a second supported top-level
// case on the same state variable. That same-state duplicate is rejected.
always_ff @(posedge clk) begin
if (rst) begin
state <= S0;
end else begin
case (state)
S0: state <= S1;
default: ;
endcase
end
case (state)
S1: state <= S2;
default: ;
endcase
end
endmodule

View File

@ -0,0 +1,6 @@
%Warning-FSMMULTI: t/t_fsmmulti_warn_bad.v:27:5: FSM coverage: multiple enum-typed case statements found in the same always block. Only the first candidate will be instrumented.
27 | case (state_b)
| ^~~~
... For warning description see https://verilator.org/warn/FSMMULTI?v=latest
... Use "/* verilator lint_off FSMMULTI */" and lint_on around source to disable this message.
%Error: Exiting due to

View File

@ -0,0 +1,19 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSMMULTI warning test
#
# 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('vlt')
test.lint(
verilator_flags2=["--coverage-fsm"],
fails=True,
expect_filename=test.golden_filename)
test.passes()

View File

@ -0,0 +1,33 @@
// DESCRIPTION: Verilator: FSMMULTI warning test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input logic clk
);
typedef enum logic [1:0] {
A0, A1
} a_state_t;
typedef enum logic [1:0] {
B0, B1
} b_state_t;
a_state_t state_a;
b_state_t state_b;
always_ff @(posedge clk) begin
case (state_a)
A0: state_a <= A1;
default: state_a <= A0;
endcase
case (state_b)
B0: state_b <= B1;
default: state_b <= B0;
endcase
end
endmodule

View File

@ -0,0 +1,17 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSMMULTI warning disabled without --coverage-fsm
#
# 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('vlt')
test.lint(verilator_flags2=["--coverage-line"])
test.file_grep_not(test.compile_log_filename, r"FSMMULTI")
test.passes()

View File

@ -0,0 +1,42 @@
// DESCRIPTION: Verilator: FSMMULTI warning disabled without --coverage-fsm
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t;
typedef enum logic [1:0] {
A0 = 2'd0,
A1 = 2'd1,
A2 = 2'd2
} state_a_t;
typedef enum logic [1:0] {
B0 = 2'd0,
B1 = 2'd1,
B2 = 2'd2
} state_b_t;
logic clk;
logic rst;
state_a_t a_state;
state_b_t b_state;
always_ff @(posedge clk) begin
if (rst) begin
a_state <= A0;
b_state <= B0;
end else begin
case (a_state)
A0: a_state <= A1;
A1: a_state <= A2;
default: a_state <= A0;
endcase
case (b_state)
B0: b_state <= B1;
B1: b_state <= B2;
default: b_state <= B0;
endcase
end
end
endmodule

View File

@ -0,0 +1,102 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM reporting coverage test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
%000007 input clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2,
S3 = 2'd3
} state_t;
integer cyc;
%000001 logic rst;
%000001 logic start;
%000003 state_t state_default /*verilator fsm_arc_include_cond*/;
%000001 state_t state_reset_incl /*verilator fsm_reset_arc*/;
%000001 state_t state_reset_excl;
%000001 initial begin
%000001 rst = 1'b1;
%000001 start = 1'b0;
%000001 cyc = 0;
end
%000007 always @(posedge clk) begin
%000007 cyc <= cyc + 1;
%000006 if (cyc == 1) rst <= 1'b0;
%000006 if (cyc == 2) start <= 1'b1;
%000006 if (cyc == 3) start <= 1'b0;
%000006 if (cyc == 6) begin
%000001 $write("*-* All Finished *-*\n");
%000001 $finish;
end
end
// This FSM gives the reporting path both ordinary arcs and a synthetic
// default arc so annotate/write-info exercise FSM-arc filtering.
%000007 always_ff @(posedge clk) begin
%000005 if (rst) begin
%000002 state_default <= S0;
%000005 end else begin
%000005 case (state_default)
// [FSM coverage]
%000001 // [fsm_arc t.state_default::ANY->S0[reset]] [reset arc, excluded from %]
%000000 // [SYNTHETIC DEFAULT ARC: t.state_default::default->S0]
%000002 // [fsm_state t.state_default::S0]
%000000 // [fsm_state t.state_default::S1] *** UNCOVERED ***
%000003 // [fsm_state t.state_default::S2]
%000000 // [fsm_state t.state_default::S3] *** UNCOVERED ***
%000003 S0: if (start) state_default <= S1; else state_default <= S2;
%000002 default: state_default <= S0;
endcase
end
end
// These two FSMs give reporting both reset-include and reset-exclude arcs so
// annotate can exercise the reset-arc filtering wording in both modes.
%000007 always_ff @(posedge clk) begin
%000005 if (rst) begin
%000002 state_reset_incl <= S0;
%000005 end else begin
%000005 case (state_reset_incl)
// [FSM coverage]
%000001 // [fsm_arc t.state_reset_incl::ANY->S0[reset_include]] [reset arc, excluded from %]
%000001 // [fsm_arc t.state_reset_incl::S0->S1]
%000000 // [fsm_state t.state_reset_incl::S0] *** UNCOVERED ***
%000001 // [fsm_state t.state_reset_incl::S1]
%000000 // [fsm_state t.state_reset_incl::S2] *** UNCOVERED ***
%000000 // [fsm_state t.state_reset_incl::S3] *** UNCOVERED ***
%000001 S0: state_reset_incl <= S1;
%000004 default: state_reset_incl <= S1;
endcase
end
end
%000007 always_ff @(posedge clk) begin
%000005 if (rst) begin
%000002 state_reset_excl <= S0;
%000005 end else begin
%000005 case (state_reset_excl)
// [FSM coverage]
%000001 // [fsm_arc t.state_reset_excl::ANY->S0[reset]] [reset arc, excluded from %]
%000001 // [fsm_arc t.state_reset_excl::S0->S1]
%000000 // [fsm_state t.state_reset_excl::S0] *** UNCOVERED ***
%000001 // [fsm_state t.state_reset_excl::S1]
%000000 // [fsm_state t.state_reset_excl::S2] *** UNCOVERED ***
%000000 // [fsm_state t.state_reset_excl::S3] *** UNCOVERED ***
%000001 S0: state_reset_excl <= S1;
%000004 default: state_reset_excl <= S1;
endcase
end
end
endmodule

View File

@ -0,0 +1,53 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM reporting coverage test
#
# 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 os
import vltest_bootstrap
test.scenarios('simulator')
# This regression targets the reporting side of FSM coverage rather than the
# detector itself. The generated coverage.dat contains state points, ordinary
# arcs, default arcs, reset arcs, and reset-include arcs so verilator_coverage
# exercises the FSM-specific filtering and annotation code paths.
test.compile(verilator_flags2=['--cc --coverage'])
test.execute()
test.run(cmd=[
os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
"--write-info",
test.obj_dir + "/coverage.info",
test.obj_dir + "/coverage.dat",
],
verilator_run=True)
test.file_grep(test.obj_dir + "/coverage.info", r"TN:verilator_coverage")
test.file_grep(test.obj_dir + "/coverage.info", r"BRF:")
test.file_grep(test.obj_dir + "/coverage.info", r"BRH:")
test.run(cmd=[os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
"--annotate", test.obj_dir + "/annotated",
test.obj_dir + "/coverage.dat"],
verilator_run=True) # yapf:disable
test.run(cmd=[os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
"--include-reset-arcs",
"--annotate", test.obj_dir + "/annotated-incl",
test.obj_dir + "/coverage.dat"],
verilator_run=True) # yapf:disable
annotated = test.obj_dir + "/annotated/t_vlcov_fsm_report.v"
annotated_incl = test.obj_dir + "/annotated-incl/t_vlcov_fsm_report.v"
test.files_identical(annotated, "t/t_vlcov_fsm_report.out")
test.files_identical(annotated_incl, "t/t_vlcov_fsm_report_incl.out")
test.passes()

View File

@ -0,0 +1,79 @@
// DESCRIPTION: Verilator: FSM reporting coverage test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2,
S3 = 2'd3
} state_t;
integer cyc;
logic rst;
logic start;
state_t state_default /*verilator fsm_arc_include_cond*/;
state_t state_reset_incl /*verilator fsm_reset_arc*/;
state_t state_reset_excl;
initial begin
rst = 1'b1;
start = 1'b0;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) rst <= 1'b0;
if (cyc == 2) start <= 1'b1;
if (cyc == 3) start <= 1'b0;
if (cyc == 6) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
// This FSM gives the reporting path both ordinary arcs and a synthetic
// default arc so annotate/write-info exercise FSM-arc filtering.
always_ff @(posedge clk) begin
if (rst) begin
state_default <= S0;
end else begin
case (state_default)
S0: if (start) state_default <= S1; else state_default <= S2;
default: state_default <= S0;
endcase
end
end
// These two FSMs give reporting both reset-include and reset-exclude arcs so
// annotate can exercise the reset-arc filtering wording in both modes.
always_ff @(posedge clk) begin
if (rst) begin
state_reset_incl <= S0;
end else begin
case (state_reset_incl)
S0: state_reset_incl <= S1;
default: state_reset_incl <= S1;
endcase
end
end
always_ff @(posedge clk) begin
if (rst) begin
state_reset_excl <= S0;
end else begin
case (state_reset_excl)
S0: state_reset_excl <= S1;
default: state_reset_excl <= S1;
endcase
end
end
endmodule

View File

@ -0,0 +1,102 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM reporting coverage test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
%000007 input clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2,
S3 = 2'd3
} state_t;
integer cyc;
%000001 logic rst;
%000001 logic start;
%000003 state_t state_default /*verilator fsm_arc_include_cond*/;
%000001 state_t state_reset_incl /*verilator fsm_reset_arc*/;
%000001 state_t state_reset_excl;
%000001 initial begin
%000001 rst = 1'b1;
%000001 start = 1'b0;
%000001 cyc = 0;
end
%000007 always @(posedge clk) begin
%000007 cyc <= cyc + 1;
%000006 if (cyc == 1) rst <= 1'b0;
%000006 if (cyc == 2) start <= 1'b1;
%000006 if (cyc == 3) start <= 1'b0;
%000006 if (cyc == 6) begin
%000001 $write("*-* All Finished *-*\n");
%000001 $finish;
end
end
// This FSM gives the reporting path both ordinary arcs and a synthetic
// default arc so annotate/write-info exercise FSM-arc filtering.
%000007 always_ff @(posedge clk) begin
%000005 if (rst) begin
%000002 state_default <= S0;
%000005 end else begin
%000005 case (state_default)
// [FSM coverage]
%000001 // [fsm_arc t.state_default::ANY->S0[reset]]
%000000 // [SYNTHETIC DEFAULT ARC: t.state_default::default->S0]
%000002 // [fsm_state t.state_default::S0]
%000000 // [fsm_state t.state_default::S1] *** UNCOVERED ***
%000003 // [fsm_state t.state_default::S2]
%000000 // [fsm_state t.state_default::S3] *** UNCOVERED ***
%000003 S0: if (start) state_default <= S1; else state_default <= S2;
%000002 default: state_default <= S0;
endcase
end
end
// These two FSMs give reporting both reset-include and reset-exclude arcs so
// annotate can exercise the reset-arc filtering wording in both modes.
%000007 always_ff @(posedge clk) begin
%000005 if (rst) begin
%000002 state_reset_incl <= S0;
%000005 end else begin
%000005 case (state_reset_incl)
// [FSM coverage]
%000001 // [fsm_arc t.state_reset_incl::ANY->S0[reset_include]]
%000001 // [fsm_arc t.state_reset_incl::S0->S1]
%000000 // [fsm_state t.state_reset_incl::S0] *** UNCOVERED ***
%000001 // [fsm_state t.state_reset_incl::S1]
%000000 // [fsm_state t.state_reset_incl::S2] *** UNCOVERED ***
%000000 // [fsm_state t.state_reset_incl::S3] *** UNCOVERED ***
%000001 S0: state_reset_incl <= S1;
%000004 default: state_reset_incl <= S1;
endcase
end
end
%000007 always_ff @(posedge clk) begin
%000005 if (rst) begin
%000002 state_reset_excl <= S0;
%000005 end else begin
%000005 case (state_reset_excl)
// [FSM coverage]
%000001 // [fsm_arc t.state_reset_excl::ANY->S0[reset]]
%000001 // [fsm_arc t.state_reset_excl::S0->S1]
%000000 // [fsm_state t.state_reset_excl::S0] *** UNCOVERED ***
%000001 // [fsm_state t.state_reset_excl::S1]
%000000 // [fsm_state t.state_reset_excl::S2] *** UNCOVERED ***
%000000 // [fsm_state t.state_reset_excl::S3] *** UNCOVERED ***
%000001 S0: state_reset_excl <= S1;
%000004 default: state_reset_excl <= S1;
endcase
end
end
endmodule