Improve FSM coverage detection (#7490)

This commit is contained in:
Yogish Sekhar 2026-04-30 12:22:34 +01:00 committed by GitHub
parent 21020ea2d1
commit 7f01806e15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 4376 additions and 599 deletions

View File

@ -34,6 +34,7 @@
#include <map>
#include <memory>
#include <unordered_map>
#include <unordered_set>
VL_DEFINE_DEBUG_FUNCTIONS;
@ -56,6 +57,69 @@ struct FsmResetCondDesc final {
AstVarScope* varScopep = nullptr;
};
class FsmResetArcDesc final {
int m_toValue = 0; // Encoded reset target state.
AstNode* m_nodep = nullptr; // Source node for warnings and emitted metadata.
public:
FsmResetArcDesc() = default;
FsmResetArcDesc(int toValue, AstNode* nodep)
: m_toValue{toValue}
, m_nodep{nodep} {}
int toValue() const { return m_toValue; }
AstNode* nodep() const { return m_nodep; }
};
class FsmRegisterCandidate final {
AstScope* m_scopep = nullptr; // Owning scope for the paired FSM.
AstAlways* m_alwaysp = nullptr; // Register process that commits the state.
AstVarScope* m_stateVscp = nullptr; // Registered FSM state variable.
AstVarScope* m_nextVscp = nullptr; // Next-state variable or same state var for 1-block FSMs.
std::vector<FsmSenDesc> m_senses; // Event controls for recreated coverage blocks.
FsmResetCondDesc m_resetCond; // Saved reset predicate, if any.
std::vector<FsmResetArcDesc> m_resetArcs; // Reset target arcs recovered during detect.
bool m_hasResetCond = false; // Whether the FSM had a modeled reset predicate.
bool m_resetInclude = false; // Whether reset arcs count toward summary totals.
bool m_inclCond = false; // Whether conditional/default arcs are kept explicitly.
public:
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; }
AstVarScope* stateVscp() const { return m_stateVscp; }
void stateVscp(AstVarScope* vscp) { m_stateVscp = vscp; }
AstVarScope* nextVscp() const { return m_nextVscp; }
void nextVscp(AstVarScope* vscp) { m_nextVscp = 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; }
const std::vector<FsmResetArcDesc>& resetArcs() const { return m_resetArcs; }
std::vector<FsmResetArcDesc>& resetArcs() { return m_resetArcs; }
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; }
};
class FsmComboAlways final {
AstScope* const m_scopep = nullptr; // Owning scope for the combinational process.
AstAlways* const m_alwaysp = nullptr; // Candidate transition process.
public:
FsmComboAlways() = default;
FsmComboAlways(AstScope* scopep, AstAlways* alwaysp)
: m_scopep{scopep}
, m_alwaysp{alwaysp} {}
AstScope* scopep() const { return m_scopep; }
AstAlways* alwaysp() const { return m_alwaysp; }
};
class FsmGraph;
class FsmVertex VL_NOT_FINAL : public V3GraphVertex {
@ -152,7 +216,7 @@ public:
// 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.
AstAlways* m_stateAlwaysp = nullptr; // Register 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.
@ -173,8 +237,8 @@ public:
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; }
AstAlways* stateAlwaysp() const { return m_stateAlwaysp; }
void stateAlwaysp(AstAlways* alwaysp) { m_stateAlwaysp = alwaysp; }
const string& stateVarName() const { return m_stateVarName; }
void stateVarName(const string& name) { m_stateVarName = name; }
const string& stateVarInternalName() const { return m_stateVarInternalName; }
@ -232,13 +296,18 @@ public:
struct DetectedFsm final {
std::unique_ptr<FsmGraph> graphp; // Extracted graph for one detected FSM candidate.
};
using DetectedFsmMap = std::map<string, DetectedFsm>;
using DetectedFsmMap = std::map<const AstVarScope*, DetectedFsm>;
struct FsmCaseCandidate final {
AstNode* warnNodep = nullptr; // Transition node that made the candidate supported.
AstVarScope* stateVscp = nullptr; // FSM state variable associated with that candidate.
};
// 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
// All detected FSMs keyed by state varscope identity. 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;
@ -258,6 +327,10 @@ class FsmDetectVisitor final : public VNVisitor {
// STATE - for current visit position (use VL_RESTORER)
FsmState& m_state;
AstScope* m_scopep = nullptr;
std::unordered_map<const AstVarScope*, FsmRegisterCandidate> m_registerCandidates;
std::vector<FsmComboAlways> m_comboAlwayss;
std::vector<FsmComboAlways> m_nonComboAlwayss;
std::unordered_map<const AstVarScope*, FsmCaseCandidate> m_comboPaired;
// METHODS
// Enum-backed FSMs may be wrapped in refs/typedefs; normalize to the
@ -266,6 +339,142 @@ class FsmDetectVisitor final : public VNVisitor {
return dtypep->skipRefToEnump();
}
static string candidateConflictContext(AstNode* laterNodep,
const FsmCaseCandidate& firstCand) {
return '\n' + laterNodep->warnContextPrimary() + firstCand.warnNodep->warnOther()
+ "... Location of first supported candidate for "
+ firstCand.stateVscp->prettyNameQ() + '\n'
+ firstCand.warnNodep->warnContextSecondary();
}
class RegisterAlwaysAnalyzer final {
AstScope* const m_scopep;
public:
explicit RegisterAlwaysAnalyzer(AstScope* scopep)
: m_scopep{scopep} {}
std::vector<std::pair<AstCase*, AstNodeExpr*>> oneBlockCandidates(AstAlways* alwaysp) const {
std::vector<std::pair<AstCase*, AstNodeExpr*>> candidates;
AstNode* const 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, FsmDetectVisitor::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);
}
return candidates;
}
bool matchRegisterCandidate(AstAlways* alwaysp, FsmRegisterCandidate& cand) const {
return FsmDetectVisitor::matchRegisterAlways(alwaysp, m_scopep, cand);
}
void buildOneBlockCandidate(AstAlways* alwaysp, AstVarScope* vscp,
AstNodeExpr* resetCondp, FsmRegisterCandidate& reg) const {
reg.scopep(m_scopep);
reg.alwaysp(alwaysp);
reg.stateVscp(vscp);
reg.nextVscp(vscp);
reg.senses() = FsmDetectVisitor::describeSenTree(alwaysp->sentreep());
reg.resetCond() = FsmDetectVisitor::describeResetCond(resetCondp);
reg.hasResetCond(reg.resetCond().varScopep != nullptr);
reg.resetInclude(vscp->varp()->attrFsmResetArc());
reg.inclCond(vscp->varp()->attrFsmArcInclCond());
AstIf* const firstIfp = VN_CAST(alwaysp->stmtsp(), If);
if (firstIfp && reg.hasResetCond()) {
AstVarScope* resetStateVscp = nullptr;
const ResetAssignStatus resetStatus = FsmDetectVisitor::collectConstStateAssigns(
firstIfp->thensp(), resetStateVscp, reg.resetArcs());
if (resetStatus == ResetAssignStatus::NONE || resetStateVscp != vscp) {
reg.resetArcs().clear();
int resetValue = 0;
AstNode* const thenNodep
= FsmDetectVisitor::singleMeaningfulBranch(firstIfp->thensp());
UASSERT_OBJ(thenNodep, firstIfp,
"one-block reset fallback requires a non-empty reset branch");
if (FsmDetectVisitor::directConstStateAssignNode(thenNodep, resetStateVscp,
resetValue)
&& resetStateVscp == vscp) {
reg.resetArcs().emplace_back(resetValue, firstIfp->thensp());
}
} else if (resetStatus == ResetAssignStatus::MULTI_SAME_STATE) {
reg.resetArcs().clear();
}
}
}
};
class ComboAlwaysAnalyzer final {
public:
struct ComboMatch final {
const FsmRegisterCandidate* matchedp = nullptr;
AstNode* warnNodep = nullptr;
};
private:
const std::unordered_map<const AstVarScope*, FsmRegisterCandidate>& m_registerCandidates;
public:
explicit ComboAlwaysAnalyzer(
const std::unordered_map<const AstVarScope*, FsmRegisterCandidate>& registerCandidates)
: m_registerCandidates{registerCandidates} {}
ComboMatch matchCase(AstNode* stmtsp, AstCase* casep) const {
ComboMatch match;
AstVarRef* const selp = VN_CAST(casep->exprp(), VarRef);
if (!selp) return match;
for (const auto& it : m_registerCandidates) {
const FsmRegisterCandidate& reg = it.second;
if (selp->varScopep() == reg.nextVscp()) {
if (!FsmDetectVisitor::hasCanonicalNextStateDefaultBeforeCase(
stmtsp, casep, reg.stateVscp(), reg.nextVscp())) {
continue;
}
} else if (selp->varScopep() != reg.stateVscp()) {
continue;
}
AstNode* const warnNodep
= FsmDetectVisitor::caseSupportedTransitionNode(casep, reg.nextVscp(),
reg.inclCond());
if (!warnNodep) continue;
match.matchedp = &reg;
match.warnNodep = warnNodep;
}
return match;
}
bool shouldWarnUnsupported(AstNode* stmtsp, AstCase* casep) const {
const AstVarRef* const selp = VN_CAST(casep->exprp(), VarRef);
if (!selp) return false;
const auto isRecognizedFsm = [&](const auto& entry) -> bool {
const FsmRegisterCandidate& reg = entry.second;
const bool matchesNext = selp->varScopep() == reg.nextVscp();
const bool matchesState = selp->varScopep() == reg.stateVscp();
if (!matchesNext && !matchesState) return false;
if (matchesNext
&& !FsmDetectVisitor::hasCanonicalNextStateDefaultBeforeCase(
stmtsp, casep, reg.stateVscp(), reg.nextVscp())) {
return false;
}
return FsmDetectVisitor::caseSupportedTransitionNode(casep, reg.nextVscp(),
reg.inclCond());
};
return std::any_of(m_registerCandidates.begin(), m_registerCandidates.end(),
isRecognizedFsm);
}
};
// 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); }
@ -303,10 +512,15 @@ class FsmDetectVisitor final : public VNVisitor {
// 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* skipLeadingIgnorableStmt(AstNode* nodep) {
while (nodep && isIgnorableStmt(nodep)) nodep = nodep->nextp();
return nodep;
}
// Conservative extractor for statement lists: only treat a list 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()) {
@ -317,12 +531,49 @@ class FsmDetectVisitor final : public VNVisitor {
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.
// If/else branches are a single subtree, not a statement list, so do not
// walk nextp() here or we may accidentally consume the sibling else-arm.
static AstNode* singleMeaningfulBranch(AstNode* branchp) {
if (!branchp) return nullptr;
return branchp;
}
// By fsm-detect time, non-clocked always @* blocks are already admitted through
// a missing sentree. This helper therefore only needs to recognize
// explicit changed-sensitivity lists such as always @(a or b); clocked and
// event-driven forms remain out of scope.
static bool isPlainComboSentree(const AstSenTree* sentreep) {
UASSERT(sentreep, "plain combo sensitivity check requires a sensitivity tree");
for (const AstSenItem* senp = sentreep->sensesp(); senp;
senp = VN_AS(senp->nextp(), SenItem)) {
if (senp->edgeType() == VEdgeType::ET_CHANGED) continue;
return false;
}
return true;
}
void warnUnsupportedComboAlways(const FsmComboAlways& combo) {
const ComboAlwaysAnalyzer analyzer{m_registerCandidates};
AstNode* const stmtsp = skipLeadingIgnorableStmt(combo.alwaysp()->stmtsp());
bool warned = false;
for (AstNode* nodep = stmtsp; nodep; nodep = nodep->nextp()) {
AstCase* const casep = VN_CAST(nodep, Case);
if (!casep) continue;
if (analyzer.shouldWarnUnsupported(stmtsp, casep)) {
casep->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on non-clocked always "
"blocks requires a combinational sensitivity list or "
"always_comb");
warned = true;
}
if (warned) break;
}
}
// Case-item bodies are single subtrees like if/else arms, not statement
// lists, so unwrap only local begin/end wrappers here rather than walking
// sibling case items via nextp().
static AstNodeAssign* directStateAssign(AstNode* stmtp, AstVarScope* stateVscp) {
AstNode* const nodep = singleMeaningfulStmt(stmtp);
AstNode* const nodep = singleMeaningfulBranch(stmtp);
if (!nodep) return nullptr;
AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign);
if (!assp) return nullptr;
@ -331,6 +582,177 @@ class FsmDetectVisitor final : public VNVisitor {
return assp;
}
static AstNodeAssign* nodeStateVarAssign(AstNode* nodep, AstVarScope*& stateVscp,
AstVarScope*& fromVscp) {
AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign);
if (!assp) return nullptr;
AstVarRef* const lhsp = VN_AS(assp->lhsp(), VarRef);
UASSERT_OBJ(lhsp, assp, "register commit lhs should be normalized to a VarRef");
AstVarRef* const rhsp = VN_CAST(assp->rhsp(), VarRef);
if (!rhsp) return nullptr;
stateVscp = lhsp->varScopep();
fromVscp = rhsp->varScopep();
return assp;
}
static AstNodeAssign* directCondStateVarAssign(AstNode* nodep, AstVarScope*& stateVscp,
AstVarScope*& fromVscp, AstNodeExpr*& condp,
int& resetValue) {
AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign);
if (!assp) return nullptr;
AstVarRef* const lhsp = VN_AS(assp->lhsp(), VarRef);
UASSERT_OBJ(lhsp, assp,
"conditional register commit lhs should be normalized to a VarRef");
AstCond* const rhsp = VN_CAST(assp->rhsp(), Cond);
if (!rhsp) return nullptr;
AstVarRef* const elsep = VN_CAST(rhsp->elsep(), VarRef);
if (!elsep || !exprConstValue(rhsp->thenp(), resetValue)) return nullptr;
stateVscp = lhsp->varScopep();
fromVscp = elsep->varScopep();
condp = rhsp->condp();
return assp;
}
static AstNodeAssign* directConstStateAssignNode(AstNode* nodep, AstVarScope*& stateVscp,
int& value) {
AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign);
if (!assp) return nullptr;
AstVarRef* const lhsp = VN_AS(assp->lhsp(), VarRef);
UASSERT_OBJ(lhsp, assp,
"direct constant state assignment lhs should be normalized to a VarRef");
if (!exprConstValue(assp->rhsp(), value)) return nullptr;
stateVscp = lhsp->varScopep();
return assp;
}
enum class ResetAssignStatus : uint8_t {
NONE, // Reset branch was not the supported direct-constant shape.
SINGLE, // Exactly one supported reset assignment was collected.
MULTI_SAME_STATE // Multiple assignments to the same FSM state var; warn and ignore.
};
// Reset arcs are only extracted from the single direct-constant form. If
// user RTL assigns the same state register multiple times in the reset
// branch, warn and skip reset-arc modeling rather than inventing multiple
// reset transitions for an odd but legal coding style.
static ResetAssignStatus collectConstStateAssigns(AstNode* stmtp, AstVarScope*& stateVscp,
std::vector<FsmResetArcDesc>& resetArcs) {
AstNode* nodep = skipLeadingIgnorableStmt(stmtp);
UASSERT_OBJ(nodep, stmtp, "Empty reset branch unexpectedly survived to FSM detection");
for (;; nodep = nodep->nextp()) {
AstVarScope* assignStateVscp = nullptr;
int value = 0;
AstNodeAssign* const assp = directConstStateAssignNode(nodep, assignStateVscp, value);
if (!assp) return ResetAssignStatus::NONE;
if (!stateVscp) stateVscp = assignStateVscp;
if (assignStateVscp != stateVscp) return ResetAssignStatus::NONE;
if (!resetArcs.empty()) {
assp->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on reset branches with "
"multiple assignments to the state variable");
resetArcs.clear();
return ResetAssignStatus::MULTI_SAME_STATE;
}
resetArcs.emplace_back(value, assp);
if (!nodep->nextp()) return ResetAssignStatus::SINGLE;
}
}
static bool hasCanonicalNextStateDefaultBeforeCase(AstNode* stmtsp, AstCase* casep,
AstVarScope* stateVscp,
AstVarScope* nextVscp) {
AstNode* const bodyp = skipLeadingIgnorableStmt(stmtsp);
bool sawCanonicalDefault = false;
for (AstNode* nodep = bodyp;; nodep = nodep->nextp()) {
UASSERT_OBJ(nodep, casep,
"case(state_d) candidate not found in scanned statement list");
if (nodep == casep) return sawCanonicalDefault;
if (AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign)) {
AstVarRef* const lhsp = VN_CAST(assp->lhsp(), VarRef);
AstVarRef* const rhsp = VN_CAST(assp->rhsp(), VarRef);
if (!lhsp || lhsp->varScopep() != nextVscp) continue;
if (sawCanonicalDefault) {
const string nextName = nextVscp->varp()->prettyNameQ();
const string stateName = stateVscp->varp()->prettyNameQ();
assp->v3warn(COVERIGN,
"Ignoring unsupported: FSM coverage on case(" + nextName
+ ") when the canonical " + nextName + " = " + stateName
+ " default is overwritten before the case statement");
return false;
}
if (!rhsp || rhsp->varScopep() != stateVscp) return false;
sawCanonicalDefault = true;
}
}
}
static bool ifStateConstAssign(AstNode* stmtp, AstVarScope* stateVscp, int& thenValue,
int& elseValue) {
AstIf* const ifp = VN_CAST(singleMeaningfulBranch(stmtp), If);
if (!ifp || !ifp->elsesp()) return false;
AstVarScope* thenVscp = nullptr;
AstVarScope* elseVscp = nullptr;
AstNode* const thenNodep = singleMeaningfulBranch(skipLeadingIgnorableStmt(ifp->thensp()));
UASSERT_OBJ(thenNodep, ifp, "Empty then-branch unexpectedly survived to FSM detection");
AstNode* const elseNodep = singleMeaningfulBranch(skipLeadingIgnorableStmt(ifp->elsesp()));
if (!elseNodep) return false;
if (!directConstStateAssignNode(thenNodep, thenVscp, thenValue)) return false;
if (!directConstStateAssignNode(elseNodep, elseVscp, elseValue)) return false;
if (thenVscp == stateVscp && elseVscp == stateVscp) return true;
if (thenVscp != elseVscp) return false;
AstNode* const followp = skipLeadingIgnorableStmt(ifp->nextp());
AstVarScope* finalStateVscp = nullptr;
AstVarScope* finalFromVscp = nullptr;
AstNode* const finalNodep = singleMeaningfulBranch(followp);
if (!finalNodep) return false;
if (!nodeStateVarAssign(finalNodep, finalStateVscp, finalFromVscp)) return false;
if (finalStateVscp != stateVscp) return false;
if (finalFromVscp != thenVscp) return false;
return true;
}
static bool directStateCondConstAssign(AstNode* stmtp, AstVarScope* stateVscp, int& thenValue,
int& elseValue) {
AstNodeAssign* const assp = directStateAssign(stmtp, stateVscp);
if (!assp) return false;
AstCond* const condp = VN_CAST(assp->rhsp(), Cond);
if (!condp) return false;
return exprConstValue(condp->thenp(), thenValue)
&& exprConstValue(condp->elsep(), elseValue);
}
static AstNode* caseItemSupportedArcNode(AstCaseItem* itemp, AstVarScope* stateVscp,
bool inclCond) {
if (itemp->isDefault()) {
if (!inclCond) return nullptr;
}
AstNodeAssign* const assp = directStateAssign(itemp->stmtsp(), stateVscp);
if (assp) {
int toValue = 0;
if (exprConstValue(assp->rhsp(), toValue)) return assp;
}
int thenValue = 0;
int elseValue = 0;
if (directStateCondConstAssign(itemp->stmtsp(), stateVscp, thenValue, elseValue)) {
return assp;
}
if (ifStateConstAssign(itemp->stmtsp(), stateVscp, thenValue, elseValue)) {
return singleMeaningfulBranch(itemp->stmtsp());
}
return nullptr;
}
// Combinational transition blocks are paired only through supported case
// items that assign to the recorded next-state variable.
static AstNode* caseSupportedTransitionNode(AstCase* casep, AstVarScope* stateVscp,
bool inclCond) {
for (AstCaseItem* itemp = casep->itemsp(); itemp;
itemp = VN_AS(itemp->nextp(), CaseItem)) {
if (AstNode* const nodep = caseItemSupportedArcNode(itemp, stateVscp, inclCond))
return nodep;
}
return nullptr;
}
// 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) {
@ -356,10 +778,105 @@ class FsmDetectVisitor final : public VNVisitor {
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");
"that assign a constant that is not present in the declared "
"enum");
return false;
}
// Strict Phase 1 matcher for register processes: either a bare state
// commit, or a top-level reset guard whose else path is that commit.
static bool matchRegisterAlways(AstAlways* alwaysp, AstScope* scopep,
FsmRegisterCandidate& cand) {
if (!alwaysp->sentreep() || !alwaysp->sentreep()->hasEdge()) return false;
AstNode* const stmtsp = skipLeadingIgnorableStmt(alwaysp->stmtsp());
AstNode* const nodep = singleMeaningfulStmt(stmtsp);
if (!nodep) return false;
AstVarScope* stateVscp = nullptr;
AstVarScope* nextVscp = nullptr;
if (AstIf* const ifp = VN_CAST(nodep, If)) {
if (!ifp->elsesp() || !isSimpleResetCond(ifp->condp())) return false;
AstVarScope* resetStateVscp = nullptr;
const ResetAssignStatus resetStatus
= collectConstStateAssigns(ifp->thensp(), resetStateVscp, cand.resetArcs());
if (resetStatus == ResetAssignStatus::NONE) {
cand.resetArcs().clear();
int resetValue = 0;
AstNode* const thenNodep = singleMeaningfulBranch(ifp->thensp());
UASSERT_OBJ(thenNodep, ifp, "reset fallback requires a non-empty reset branch");
if (!directConstStateAssignNode(thenNodep, resetStateVscp, resetValue))
return false;
cand.resetArcs().emplace_back(resetValue, ifp->thensp());
} else if (resetStatus == ResetAssignStatus::MULTI_SAME_STATE) {
cand.resetArcs().clear();
}
AstNode* const elseNodep = singleMeaningfulBranch(ifp->elsesp());
UASSERT_OBJ(elseNodep, ifp, "register reset match requires a non-empty commit branch");
if (!nodeStateVarAssign(elseNodep, stateVscp, nextVscp)) return false;
if (resetStateVscp != stateVscp) return false;
cand.resetCond() = describeResetCond(ifp->condp());
cand.hasResetCond(cand.resetCond().varScopep != nullptr);
} else {
AstNodeExpr* resetCondp = nullptr;
int resetValue = 0;
if (AstNodeAssign* const assp
= directCondStateVarAssign(nodep, stateVscp, nextVscp, resetCondp, resetValue)) {
cand.resetArcs().emplace_back(resetValue, assp);
cand.resetCond() = describeResetCond(resetCondp);
cand.hasResetCond(cand.resetCond().varScopep != nullptr);
} else if (!nodeStateVarAssign(nodep, stateVscp, nextVscp)) {
return false;
}
}
cand.scopep(scopep);
cand.alwaysp(alwaysp);
cand.stateVscp(stateVscp);
cand.nextVscp(nextVscp);
cand.senses() = describeSenTree(alwaysp->sentreep());
cand.resetInclude(stateVscp->varp()->attrFsmResetArc());
cand.inclCond(stateVscp->varp()->attrFsmArcInclCond());
return true;
}
// Build the Phase 1 state space from the tracked registered state
// variable, not from whichever signal the transition case happened to use.
static bool collectStateLabels(AstNode* nodep, AstVarScope* stateVscp,
std::vector<std::pair<string, int>>& states,
std::unordered_map<int, string>& labels) {
AstVar* const stateVarp = stateVscp->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 false;
if (enump) {
if (stateVscp->width() > 32) {
nodep->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on enum-typed state "
"variables wider than 32 bits");
return false;
}
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());
}
return states.size() >= 2;
}
const int width = stateVarp->width();
if (width >= 31) return false;
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);
}
return true;
}
// 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
@ -390,24 +907,21 @@ class FsmDetectVisitor final : public VNVisitor {
}
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;
int thenValue = 0;
int elseValue = 0;
if (directStateCondConstAssign(itemp->stmtsp(), stateVscp, thenValue, elseValue)
|| ifStateConstAssign(itemp->stmtsp(), stateVscp, thenValue, elseValue)) {
if (!validateKnownStateValue(itemp->stmtsp(), labels, thenValue)) return true;
if (!validateKnownStateValue(itemp->stmtsp(), 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(),
itemp->stmtsp()->fileline());
}
}
return true;
}
return false;
@ -415,82 +929,45 @@ class FsmDetectVisitor final : public VNVisitor {
// 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,
static void addResetArcs(FsmGraph& graph, const std::vector<FsmResetArcDesc>& resetArcs,
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());
}
}
for (const FsmResetArcDesc& resetArc : resetArcs) {
if (!validateKnownStateValue(resetArc.nodep(), labels, resetArc.toValue())) continue;
graph.addArc(0, resetArc.toValue(), true, false, false, resetArc.nodep()->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;
void processCase(AstCase* casep, AstVarScope* assignVscp, const FsmRegisterCandidate& reg) {
UASSERT_OBJ(assignVscp, casep, "FSM case processing requires a non-null assignment var");
AstVarScope* const stateVscp = reg.stateVscp();
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 (!collectStateLabels(casep, stateVscp, states, labels)) return;
DetectedFsm& entry = m_state.fsms()[stateVscp];
if (!entry.graphp) {
entry.graphp.reset(new FsmGraph{});
entry.graphp->scopep(m_scopep);
entry.graphp->alwaysp(alwaysp);
entry.graphp->scopep(reg.scopep());
entry.graphp->stateAlwaysp(reg.alwaysp());
entry.graphp->stateVarName(stateVscp->prettyName());
entry.graphp->stateVarInternalName(stateVarp->name());
entry.graphp->stateVarInternalName(stateVscp->varp()->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->senses() = reg.senses();
entry.graphp->resetCond() = reg.resetCond();
entry.graphp->hasResetCond(reg.hasResetCond());
entry.graphp->resetInclude(reg.resetInclude());
entry.graphp->inclCond(reg.inclCond());
entry.graphp->fileline(casep->fileline());
for (const std::pair<string, int>& state : states) {
entry.graphp->addStateVertex(state.first, state.second);
}
addResetArcs(*entry.graphp, reg.resetArcs(), labels);
}
for (AstCaseItem* itemp = casep->itemsp(); itemp;
itemp = VN_AS(itemp->nextp(), CaseItem)) {
emitCaseItemArcs(*entry.graphp, itemp, stateVscp, labels, entry.graphp->inclCond());
emitCaseItemArcs(*entry.graphp, itemp, assignVscp, labels, entry.graphp->inclCond());
}
}
@ -499,57 +976,93 @@ class FsmDetectVisitor final : public VNVisitor {
// 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);
}
void processOneBlockAlways(AstAlways* alwaysp) {
const RegisterAlwaysAnalyzer analyzer{m_scopep};
if (!alwaysp->sentreep() || !alwaysp->sentreep()->hasEdge()) return;
const std::vector<std::pair<AstCase*, AstNodeExpr*>> candidates
= analyzer.oneBlockCandidates(alwaysp);
if (candidates.empty()) return;
AstVarScope* firstVscp = nullptr;
FsmCaseCandidate firstCand;
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) {
if (!firstCand.stateVscp) {
firstCand.warnNodep = cand.first;
firstCand.stateVscp = vscp;
FsmRegisterCandidate reg;
analyzer.buildOneBlockCandidate(alwaysp, vscp, cand.second, reg);
processCase(cand.first, vscp, reg);
} else if (vscp != firstCand.stateVscp) {
cand.first->v3warn(FSMMULTI,
"FSM coverage: multiple enum-typed case statements found in "
"the same always block. Only the first candidate will be "
"instrumented.");
"instrumented."
<< candidateConflictContext(cand.first, firstCand));
} 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.");
"candidate will be instrumented."
<< candidateConflictContext(cand.first, firstCand));
}
}
}
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());
// Phase 1 two-process pairing scans combinational always blocks only after
// all strict register candidates have been collected, so source order does
// not matter.
static void warnComboSameAlways(AstNode* warnNodep, const FsmCaseCandidate& firstCand) {
warnNodep->v3warn(FSMMULTI,
"FSM coverage: multiple supported transition candidates found in "
"the same combinational always block. Only the first candidate "
"will be instrumented."
<< candidateConflictContext(warnNodep, firstCand));
}
void processComboAlways(const FsmComboAlways& combo) {
const ComboAlwaysAnalyzer analyzer{m_registerCandidates};
AstNode* const stmtsp = skipLeadingIgnorableStmt(combo.alwaysp()->stmtsp());
FsmCaseCandidate firstCand;
for (AstNode* nodep = stmtsp; nodep; nodep = nodep->nextp()) {
AstCase* const casep = VN_CAST(nodep, Case);
if (!casep) continue;
const ComboAlwaysAnalyzer::ComboMatch match = analyzer.matchCase(stmtsp, casep);
const FsmRegisterCandidate* const matchedp = match.matchedp;
AstNode* const matchedWarnNodep = match.warnNodep;
if (!matchedp) continue;
if (!firstCand.stateVscp) {
const auto insertPair = m_comboPaired.emplace(
matchedp->stateVscp(), FsmCaseCandidate{matchedWarnNodep,
const_cast<AstVarScope*>(
matchedp->stateVscp())});
if (!insertPair.second) {
matchedWarnNodep->v3warn(
FSMMULTI,
"FSM coverage: multiple supported transition candidates found "
"for the same FSM in combinational always blocks. Only the "
"first candidate will be instrumented."
<< candidateConflictContext(matchedWarnNodep, insertPair.first->second));
continue;
}
firstCand.warnNodep = matchedWarnNodep;
firstCand.stateVscp = const_cast<AstVarScope*>(matchedp->stateVscp());
processCase(casep, matchedp->nextVscp(), *matchedp);
continue;
}
if (matchedp->stateVscp() != firstCand.stateVscp) {
warnComboSameAlways(matchedWarnNodep, firstCand);
continue;
}
matchedWarnNodep->v3warn(COVERIGN,
"Ignoring unsupported: FSM coverage on multiple "
"supported case statements found in the same "
"combinational always block. Only the first "
"candidate will be instrumented."
<< candidateConflictContext(matchedWarnNodep,
firstCand));
}
addResetArcs(*graphp, firstIfp->thensp(), firstVscp, labels);
}
// Track the current scope so each detected FSM records the module/scope
@ -560,8 +1073,26 @@ class FsmDetectVisitor final : public VNVisitor {
iterateChildren(nodep);
}
// FSM extraction only cares about clocked always processes.
void visit(AstAlways* nodep) override { processAlways(nodep); }
// Collect one-block FSMs immediately, strict register candidates for later
// pairing, and combinational processes for the second-stage transition
// scan.
void visit(AstAlways* nodep) override {
processOneBlockAlways(nodep);
const RegisterAlwaysAnalyzer analyzer{m_scopep};
FsmRegisterCandidate reg;
if (analyzer.matchRegisterCandidate(nodep, reg)) {
m_registerCandidates.emplace(reg.stateVscp(), reg);
}
if (nodep->keyword() == VAlwaysKwd::ALWAYS_COMB) {
m_comboAlwayss.emplace_back(m_scopep, nodep);
} else if (nodep->keyword() == VAlwaysKwd::ALWAYS) {
if (!nodep->sentreep() || isPlainComboSentree(nodep->sentreep())) {
m_comboAlwayss.emplace_back(m_scopep, nodep);
} else {
m_nonComboAlwayss.emplace_back(m_scopep, nodep);
}
}
}
// Continue the walk through the rest of the design hierarchy.
void visit(AstNode* nodep) override { iterateChildren(nodep); }
@ -573,6 +1104,8 @@ public:
FsmDetectVisitor(FsmState& state, AstNetlist* rootp)
: m_state{state} {
iterate(rootp);
for (const FsmComboAlways& combo : m_comboAlwayss) processComboAlways(combo);
for (const FsmComboAlways& combo : m_nonComboAlwayss) warnUnsupportedComboAlways(combo);
}
};
@ -621,7 +1154,9 @@ class FsmLowerVisitor final {
// 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();
UINFO(1, "buildOne lowering FSM " << graph.stateVarName()
<< " vertices=" << graph.vertices().size() << endl);
AstAlways* const alwaysp = graph.stateAlwaysp();
AstScope* const scopep = graph.scopep();
AstVarScope* const stateVscp = graph.stateVarScopep();
FileLine* const flp = graph.fileline();
@ -761,9 +1296,7 @@ public:
// 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);
}
for (const auto& it : m_state.fsms()) { buildOne(*it.second.graphp); }
}
};
@ -777,7 +1310,7 @@ void V3FsmDetect::detect(AstNetlist* rootp) {
FsmDetectVisitor detect{state, rootp};
if (dumpGraphLevel() >= 6) {
size_t index = 0;
for (const std::pair<const string, DetectedFsm>& it : state.fsms()) {
for (const auto& it : state.fsms()) {
it.second.graphp->dumpDotFilePrefixed(it.second.graphp->dumpTag(index++));
}
}

View File

@ -17,16 +17,6 @@ test.compile(verilator_flags2=['--cc --coverage-fsm'])
test.execute()
test.run(cmd=[
os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
test.obj_dir + "/coverage.dat",
],
logfile=test.obj_dir + "/summary.log",
tee=False,
verilator_run=True)
test.files_identical(test.obj_dir + "/summary.log", "t/" + test.name + "_summary.out")
test.run(cmd=[
os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
"--annotate",

View File

@ -1,7 +0,0 @@
Coverage Summary:
line : 0.0% (0/0)
toggle : 0.0% (0/0)
branch : 0.0% (0/0)
expr : 0.0% (0/0)
fsm_state : 50.0% (2/4)
fsm_arc : 100.0% (5/5)

View File

@ -53,11 +53,20 @@
%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;
S0: begin
if (sel) begin
state <= S1;
end
else begin
state <= S2;
end
end
S1: begin
state <= S0;
end
default: begin
state <= S0;
end
endcase
end
end

View File

@ -44,11 +44,20 @@ module t (
end
else begin
case (state)
S0:
if (sel) state <= S1;
else state <= S2;
S1: state <= S0;
default: state <= S0;
S0: begin
if (sel) begin
state <= S1;
end
else begin
state <= S2;
end
end
S1: begin
state <= S0;
end
default: begin
state <= S0;
end
endcase
end
end

View File

@ -0,0 +1,122 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM coverage keeps grouped canonical case(state_d) forms
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Group accepted canonical case(state_d) forms so supported next-state
// defaults stay separate from the rejected wrong-RHS and tail-mismatch shapes.
module fsm_case_next_other_assign_ok (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'b00,
S1 = 2'b01,
S2 = 2'b10
} state_t;
logic [1:0] aux;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
initial aux = 2'b00;
always_comb begin
aux[0] = start;
state_d = state_q;
%000004 case (state_d)
// [FSM coverage]
%000001 // [fsm_arc t.case_next_other_assign_ok_u.state_q::ANY->S0[reset_include]] [reset arc, excluded from %]
%000000 // [fsm_arc t.case_next_other_assign_ok_u.state_q::S0->S1]
%000004 // [fsm_arc t.case_next_other_assign_ok_u.state_q::S0->S2]
%000003 // [fsm_state t.case_next_other_assign_ok_u.state_q::S0]
%000000 // [fsm_state t.case_next_other_assign_ok_u.state_q::S1] *** UNCOVERED ***
%000004 // [fsm_state t.case_next_other_assign_ok_u.state_q::S2]
S0: state_d = start ? S1 : S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) begin
state_q <= S0;
end else begin
state_q <= state_d;
end
end
endmodule
module fsm_case_next_other_lhs_ok (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'b00,
S1 = 2'b01,
S2 = 2'b10
} state_t;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
state_t other_d;
always_comb begin
other_d = state_q;
state_d = state_q;
%000004 case (state_d)
// [FSM coverage]
%000001 // [fsm_arc t.case_next_other_lhs_ok_u.state_q::ANY->S0[reset_include]] [reset arc, excluded from %]
%000000 // [fsm_arc t.case_next_other_lhs_ok_u.state_q::S0->S1]
%000004 // [fsm_arc t.case_next_other_lhs_ok_u.state_q::S0->S2]
%000003 // [fsm_state t.case_next_other_lhs_ok_u.state_q::S0]
%000000 // [fsm_state t.case_next_other_lhs_ok_u.state_q::S1] *** UNCOVERED ***
%000004 // [fsm_state t.case_next_other_lhs_ok_u.state_q::S2]
S0: state_d = start ? S1 : S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) begin
state_q <= S0;
end else begin
state_q <= state_d;
end
end
endmodule
module t (
input logic clk
);
logic rst;
logic start;
integer cyc;
fsm_case_next_other_assign_ok case_next_other_assign_ok_u (
.clk(clk), .rst(rst), .start(start));
fsm_case_next_other_lhs_ok case_next_other_lhs_ok_u (
.clk(clk), .rst(rst), .start(start));
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
endmodule

View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage keeps grouped canonical case(state_d) forms
#
# 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.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)
test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename)
test.files_identical(test.obj_dir + "/annotated-incl/" + test.name + ".v",
"t/" + test.name + "_incl.out")
test.passes()

View File

@ -0,0 +1,106 @@
// DESCRIPTION: Verilator: FSM coverage keeps grouped canonical case(state_d) forms
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Group accepted canonical case(state_d) forms so supported next-state
// defaults stay separate from the rejected wrong-RHS and tail-mismatch shapes.
module fsm_case_next_other_assign_ok (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'b00,
S1 = 2'b01,
S2 = 2'b10
} state_t;
logic [1:0] aux;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
initial aux = 2'b00;
always_comb begin
aux[0] = start;
state_d = state_q;
case (state_d)
S0: state_d = start ? S1 : S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) begin
state_q <= S0;
end else begin
state_q <= state_d;
end
end
endmodule
module fsm_case_next_other_lhs_ok (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'b00,
S1 = 2'b01,
S2 = 2'b10
} state_t;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
state_t other_d;
always_comb begin
other_d = state_q;
state_d = state_q;
case (state_d)
S0: state_d = start ? S1 : S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) begin
state_q <= S0;
end else begin
state_q <= state_d;
end
end
endmodule
module t (
input logic clk
);
logic rst;
logic start;
integer cyc;
fsm_case_next_other_assign_ok case_next_other_assign_ok_u (
.clk(clk), .rst(rst), .start(start));
fsm_case_next_other_lhs_ok case_next_other_lhs_ok_u (
.clk(clk), .rst(rst), .start(start));
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
endmodule

View File

@ -0,0 +1,122 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM coverage keeps grouped canonical case(state_d) forms
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Group accepted canonical case(state_d) forms so supported next-state
// defaults stay separate from the rejected wrong-RHS and tail-mismatch shapes.
module fsm_case_next_other_assign_ok (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'b00,
S1 = 2'b01,
S2 = 2'b10
} state_t;
logic [1:0] aux;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
initial aux = 2'b00;
always_comb begin
aux[0] = start;
state_d = state_q;
%000004 case (state_d)
// [FSM coverage]
%000001 // [fsm_arc t.case_next_other_assign_ok_u.state_q::ANY->S0[reset_include]]
%000000 // [fsm_arc t.case_next_other_assign_ok_u.state_q::S0->S1]
%000004 // [fsm_arc t.case_next_other_assign_ok_u.state_q::S0->S2]
%000003 // [fsm_state t.case_next_other_assign_ok_u.state_q::S0]
%000000 // [fsm_state t.case_next_other_assign_ok_u.state_q::S1] *** UNCOVERED ***
%000004 // [fsm_state t.case_next_other_assign_ok_u.state_q::S2]
S0: state_d = start ? S1 : S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) begin
state_q <= S0;
end else begin
state_q <= state_d;
end
end
endmodule
module fsm_case_next_other_lhs_ok (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'b00,
S1 = 2'b01,
S2 = 2'b10
} state_t;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
state_t other_d;
always_comb begin
other_d = state_q;
state_d = state_q;
%000004 case (state_d)
// [FSM coverage]
%000001 // [fsm_arc t.case_next_other_lhs_ok_u.state_q::ANY->S0[reset_include]]
%000000 // [fsm_arc t.case_next_other_lhs_ok_u.state_q::S0->S1]
%000004 // [fsm_arc t.case_next_other_lhs_ok_u.state_q::S0->S2]
%000003 // [fsm_state t.case_next_other_lhs_ok_u.state_q::S0]
%000000 // [fsm_state t.case_next_other_lhs_ok_u.state_q::S1] *** UNCOVERED ***
%000004 // [fsm_state t.case_next_other_lhs_ok_u.state_q::S2]
S0: state_d = start ? S1 : S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) begin
state_q <= S0;
end else begin
state_q <= state_d;
end
end
endmodule
module t (
input logic clk
);
logic rst;
logic start;
integer cyc;
fsm_case_next_other_assign_ok case_next_other_assign_ok_u (
.clk(clk), .rst(rst), .start(start));
fsm_case_next_other_lhs_ok case_next_other_lhs_ok_u (
.clk(clk), .rst(rst), .start(start));
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
endmodule

View File

@ -0,0 +1,9 @@
%Warning-COVERIGN: t/t_cover_fsm_combo_same_warn_bad.v:28:19: Ignoring unsupported: FSM coverage on multiple supported case statements found in the same combinational always block. Only the first candidate will be instrumented.
28 | S1: state_d = S2;
| ^
t/t_cover_fsm_combo_same_warn_bad.v:24:19: ... Location of first supported candidate for 't.state_q'
24 | S0: state_d = S1;
| ^
... 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,21 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: same-FSM combo multi-case 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')
# Two supported case statements in the same combinational always block for the
# same FSM are legal RTL, but Phase 1 only instruments the first and warns on
# the later duplicate.
test.compile(verilator_flags2=["--cc --coverage"],
fails=True,
expect_filename=test.golden_filename)
test.passes()

View File

@ -0,0 +1,38 @@
// DESCRIPTION: Verilator: same-FSM combo multi-case 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,
input logic rst
);
typedef enum logic [1:0] {
S0,
S1,
S2
} state_t;
state_t state_q;
state_t state_d;
always_comb begin
state_d = state_q;
case (state_q)
S0: state_d = S1;
default: ;
endcase
case (state_q)
S1: state_d = S2;
default: ;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule

View File

@ -12,7 +12,7 @@ from pathlib import Path
import vltest_bootstrap
test.scenarios('vlt')
test.top_filename = "t/t_cover_fsm_styles.v"
test.top_filename = "t/t_cover_fsm_policy_accept_multi.v"
# Dump the lowered AST so AstCoverOtherDecl::dump() sees FSM metadata-bearing
# coverage declarations directly. This avoids JSON/schema coupling while still
@ -22,7 +22,7 @@ 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 " fv=t.style_u.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)

View File

@ -1,6 +0,0 @@
%Warning-COVERIGN: t/t_cover_fsm_enum_bad.v:29:19: Ignoring unsupported: FSM coverage on enum state transitions that assign a constant not present in the declared enum
29 | 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

@ -1,36 +0,0 @@
// 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

@ -1,52 +0,0 @@
// // 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

@ -1,42 +0,0 @@
// 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

@ -10,7 +10,7 @@
import vltest_bootstrap
test.scenarios('vltmt')
test.top_filename = "t/t_cover_fsm_styles.v"
test.top_filename = "t/t_cover_fsm_policy_accept_multi.v"
test.compile(v_flags2=["--coverage-fsm", "--dumpi-graph", "6"], threads=2)

View File

@ -0,0 +1,15 @@
%Warning-COVERIGN: t/t_cover_fsm_if_unknown_enum_multi_bad.v:26:19: Ignoring unsupported: FSM coverage on enum state transitions that assign a constant that is not present in the declared enum
26 | S0: state_d = sel ? 2'd3 : S1;
| ^
... 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.
%Warning-COVERIGN: t/t_cover_fsm_if_unknown_enum_multi_bad.v:53:19: Ignoring unsupported: FSM coverage on enum state transitions that assign a constant that is not present in the declared enum
53 | S0: state_d = sel ? S1 : 2'd3;
| ^
%Warning-COVERIGN: t/t_cover_fsm_if_unknown_enum_multi_bad.v:79:19: Ignoring unsupported: FSM coverage on enum state transitions that assign a constant that is not present in the declared enum
79 | S0: state_d = 2'd3;
| ^
%Warning-COVERIGN: t/t_cover_fsm_if_unknown_enum_multi_bad.v:126:15: Ignoring unsupported: FSM coverage on enum state transitions that assign a constant that is not present in the declared enum
126 | state_q <= 2'd3;
| ^~
%Error: Exiting due to

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM enum transition bad-value test
# DESCRIPTION: Verilator: FSM direct and conditional transition bad enum tests
#
# 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
@ -11,9 +11,6 @@ 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, expect_filename=test.golden_filename)
test.passes()

View File

@ -0,0 +1,140 @@
// DESCRIPTION: Verilator: FSM rejects unknown enum constants in direct and conditional transitions
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Group the same unsupported warning family across direct, conditional, and
// reset-driven enum assignments that use constants outside the declared enum.
module unknown_then (
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1
} state_t;
logic sel;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
always_comb begin
state_d = state_q;
case (state_q)
/* verilator lint_off ENUMVALUE */
S0: state_d = sel ? 2'd3 : S1;
/* verilator lint_on ENUMVALUE */
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
endmodule
module unknown_else (
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1
} state_t;
logic sel;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
always_comb begin
state_d = state_q;
case (state_q)
/* verilator lint_off ENUMVALUE */
S0: state_d = sel ? S1 : 2'd3;
/* verilator lint_on ENUMVALUE */
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
endmodule
module unknown_direct (
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1
} state_t;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
always_comb begin
state_d = state_q;
case (state_q)
/* verilator lint_off ENUMVALUE */
S0: state_d = 2'd3;
/* verilator lint_on ENUMVALUE */
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
endmodule
module unknown_reset (
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1
} state_t;
logic rst;
integer cyc;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
initial begin
rst = 1'b1;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) rst <= 1'b0;
end
always_comb begin
state_d = state_q;
case (state_q)
S0: state_d = S1;
default: state_d = S0;
endcase
end
// Reset-arc extraction uses a separate path from steady-state case-item arc
// extraction, so keep one reset-only bad enum target in this grouped test.
always_ff @(posedge clk) begin
if (rst) begin
/* verilator lint_off ENUMVALUE */
state_q <= 2'd3;
/* verilator lint_on ENUMVALUE */
end else begin
state_q <= state_d;
end
end
endmodule
module t;
logic clk;
unknown_then unknown_then_u(.clk(clk));
unknown_else unknown_else_u(.clk(clk));
unknown_direct unknown_direct_u(.clk(clk));
unknown_reset unknown_reset_u(.clk(clk));
endmodule

View File

@ -0,0 +1,6 @@
%Warning-COVERIGN: t/t_cover_fsm_nextstate_overwrite_warn.v:21:13: Ignoring unsupported: FSM coverage on case('t.state_d') when the canonical 't.state_d' = 't.state_q' default is overwritten before the case statement
21 | state_d = (state_d == S0) ? S1 : S0;
| ^
... 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,21 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM case(state_d) overwritten-default 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')
# A canonical `state_d = state_q` default followed by another top-level write
# to `state_d` before `case (state_d)` is legal RTL, but it is not the narrow
# Phase 1 shape we support. Warn and reject rather than silently skipping it.
test.compile(verilator_flags2=["--cc --coverage"],
fails=True,
expect_filename=test.golden_filename)
test.passes()

View File

@ -0,0 +1,32 @@
// DESCRIPTION: Verilator: FSM case(state_d) overwritten-default 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,
input logic rst
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1
} state_t;
state_t state_q;
state_t state_d;
always_comb begin
state_d = state_q;
state_d = (state_d == S0) ? S1 : S0;
case (state_d)
S0: state_d = S1;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule

View File

@ -0,0 +1 @@
# SystemC::Coverage-3

View File

@ -0,0 +1,20 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage ignores grouped non-matching plain always shapes
#
# 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-fsm', '-Werror-COVERIGN'])
test.execute()
test.files_identical(test.obj_dir + "/coverage.dat", "t/" + test.name + ".out")
test.passes()

View File

@ -0,0 +1,99 @@
// DESCRIPTION: Verilator: FSM coverage ignores non-matching plain always scan shapes
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Group plain-always warning-scan shapes that should be ignored silently
// before the detector ever decides an FSM candidate is present.
module ignore_sel_expr (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
// Plain always with a non-VarRef selector should be ignored silently.
always @(posedge clk) begin
state_d = state_q;
case (state_q + 1)
S0: state_d = start ? S1 : S0;
S1: state_d = S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module ignore_other_selector (
input logic clk,
input logic rst,
input logic start,
input logic other
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
// Plain always with an unrelated selector should also be ignored silently.
always @(posedge clk) begin
state_d = state_q;
case (other)
1'b0: state_d = start ? S1 : S0;
default: state_d = S2;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module t (
input logic clk
);
logic rst;
logic start;
logic other;
integer cyc;
ignore_sel_expr ignore_sel_expr_u(.*);
ignore_other_selector ignore_other_selector_u(.*);
initial begin
rst = 1'b1;
start = 1'b0;
other = 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 == 4) other <= 1'b1;
if (cyc == 8) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
endmodule

View File

@ -0,0 +1,12 @@
%Warning-COVERIGN: t/t_cover_fsm_plain_always_warn_multi_bad.v:25:5: Ignoring unsupported: FSM coverage on non-clocked always blocks requires a combinational sensitivity list or always_comb
25 | case (state_q)
| ^~~~
... 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.
%Warning-COVERIGN: t/t_cover_fsm_plain_always_warn_multi_bad.v:55:5: Ignoring unsupported: FSM coverage on non-clocked always blocks requires a combinational sensitivity list or always_comb
55 | case (state_d)
| ^~~~
%Warning-COVERIGN: t/t_cover_fsm_plain_always_warn_multi_bad.v:86:5: Ignoring unsupported: FSM coverage on non-clocked always blocks requires a combinational sensitivity list or always_comb
86 | case (state_q)
| ^~~~
%Error: Exiting due to

View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage warns on grouped non-clocked always near-FSM shapes
#
# 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.compile(verilator_flags2=['--cc --coverage-fsm'],
fails=True,
expect_filename=test.golden_filename)
test.passes()

View File

@ -0,0 +1,106 @@
// DESCRIPTION: Verilator: FSM coverage warns on non-clocked always near-FSM shapes
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module warn_edge (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
// Plain edge-sensitive next-state logic should warn even when the selector
// is the canonical state_q form.
always @(posedge clk) begin
state_d = state_q;
case (state_q)
S0: state_d = start ? S1 : S0;
S1: state_d = S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module warn_case_next (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
// Plain always also warns for the near-supported case(state_d) style.
always @(posedge clk) begin
state_d = state_q;
case (state_d)
S0: state_d = start ? S1 : S0;
S1: state_d = S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module warn_default_incl (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q /*verilator fsm_reset_arc*/ /*verilator fsm_arc_include_cond*/;
state_t state_d;
// default becomes supported only because include_cond is set, but the block
// still warns because it is a plain edge-sensitive always.
always @(posedge clk) begin
state_d = state_q;
case (state_q)
default: state_d = S0;
S0: state_d = start ? state_q : state_q;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module t;
logic clk;
logic rst;
logic start;
warn_edge warn_edge_u(.*);
warn_case_next warn_case_next_u(.*);
warn_default_incl warn_default_incl_u(.*);
endmodule

View File

@ -0,0 +1,9 @@
# SystemC::Coverage-3
C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl28n5tfsm_arcpagev_fsm_arc/$rootot.near_canonical_state_d_case_u.state_d::S0->S1Fvt.near_canonical_state_d_case_u.state_dFfS0FtS1htop.TOP' 0
C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl28n5tfsm_arcpagev_fsm_arc/$rootot.near_canonical_state_d_case_u.state_d::S0->S2Fvt.near_canonical_state_d_case_u.state_dFfS0FtS2htop.TOP' 0
C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl28n5tfsm_statepagev_fsm_state/$rootot.near_canonical_state_d_case_u.state_d::S0Fvt.near_canonical_state_d_case_u.state_dFtS0htop.TOP' 0
C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl28n5tfsm_statepagev_fsm_state/$rootot.near_canonical_state_d_case_u.state_d::S1Fvt.near_canonical_state_d_case_u.state_dFtS1htop.TOP' 0
C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl28n5tfsm_statepagev_fsm_state/$rootot.near_canonical_state_d_case_u.state_d::S2Fvt.near_canonical_state_d_case_u.state_dFtS2htop.TOP' 0
C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl60n5tfsm_statepagev_fsm_state/$rootot.selector_matches_noassign_u.state_q::S0Fvt.selector_matches_noassign_u.state_qFtS0htop.TOP' 0
C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl60n5tfsm_statepagev_fsm_state/$rootot.selector_matches_noassign_u.state_q::S1Fvt.selector_matches_noassign_u.state_qFtS1htop.TOP' 0
C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl60n5tfsm_statepagev_fsm_state/$rootot.selector_matches_noassign_u.state_q::S2Fvt.selector_matches_noassign_u.state_qFtS2htop.TOP' 0

View File

@ -0,0 +1,20 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage keeps zero-hit records for near-miss plain always shapes
#
# 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-fsm', '-Werror-COVERIGN'])
test.execute()
test.files_identical(test.obj_dir + "/coverage.dat", "t/" + test.name + ".out")
test.passes()

View File

@ -0,0 +1,98 @@
// DESCRIPTION: Verilator: FSM coverage keeps zero-hit records for near-miss plain always shapes
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Group plain-always near misses that still recover enough structure to leave
// zero-hit FSM records behind, but must not warn or model a supported FSM.
module near_canonical_state_d_case (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
// Unsupported for the plain-always warning scan: case(state_d) is only
// near-supported when it follows the canonical state_d = state_q default.
always @(posedge clk) begin
state_d = S1;
case (state_d)
S0: state_d = start ? S1 : S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module selector_matches_noassign (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
logic other;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
// The selector matches the FSM state, but the case body never assigns
// state_d, so the warning scan should ignore it while leaving zero-hit
// state points behind.
always @(posedge clk) begin
state_d = state_q;
case (state_q)
S0: other = start;
default: other = 1'b0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module t (
input logic clk
);
logic rst;
logic start;
integer cyc;
near_canonical_state_d_case near_canonical_state_d_case_u(.*);
selector_matches_noassign selector_matches_noassign_u(.*);
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
endmodule

View File

@ -0,0 +1,141 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM coverage keeps grouped accepted policy-style forms
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Group accepted policy-driven forms that should stay inferred, even though
// they exercise different coverage attributes or non-enum state policies.
module fsm_style_incl (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2,
S3 = 2'd3
} state_t;
state_t state /*verilator fsm_arc_include_cond*/;
always_ff @(posedge clk) begin
if (rst) begin
state <= S0;
end else begin
%000003 case (state)
// [FSM coverage]
%000001 // [fsm_arc t.style_u.state::ANY->S0[reset]] [reset arc, excluded from %]
%000000 // [fsm_arc t.style_u.state::S0->S1]
%000003 // [fsm_arc t.style_u.state::S0->S2]
%000000 // [fsm_arc t.style_u.state::S1->S3]
%000000 // [SYNTHETIC DEFAULT ARC: t.style_u.state::default->S0]
%000002 // [fsm_state t.style_u.state::S0]
%000000 // [fsm_state t.style_u.state::S1] *** UNCOVERED ***
%000003 // [fsm_state t.style_u.state::S2]
%000000 // [fsm_state t.style_u.state::S3] *** UNCOVERED ***
S0:
if (start) state <= S1;
else state <= S2;
S1: state <= S3;
default: state <= S0;
endcase
end
end
endmodule
module fsm_default_incl_ok (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q /*verilator fsm_arc_include_cond*/;
state_t state_d;
always_comb begin
state_d = state_q;
%000003 case (state_q)
// [FSM coverage]
%000001 // [fsm_arc t.default_incl_u.state_q::ANY->S0[reset]] [reset arc, excluded from %]
%000000 // [fsm_arc t.default_incl_u.state_q::S0->S1]
%000003 // [fsm_arc t.default_incl_u.state_q::S0->S2]
%000000 // [SYNTHETIC DEFAULT ARC: t.default_incl_u.state_q::default->S0]
%000002 // [fsm_state t.default_incl_u.state_q::S0]
%000000 // [fsm_state t.default_incl_u.state_q::S1] *** UNCOVERED ***
%000003 // [fsm_state t.default_incl_u.state_q::S2]
S0: state_d = start ? S1 : S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module fsm_forced_ok (
input logic clk,
input logic rst
);
logic [1:0] state /*verilator fsm_state*/;
always_ff @(posedge clk) begin
if (rst) begin
state <= 2'd0;
end else begin
%000002 case (state)
// [FSM coverage]
%000001 // [fsm_arc t.forced_u.state::ANY->S0[reset]] [reset arc, excluded from %]
%000002 // [fsm_arc t.forced_u.state::S0->S1]
%000002 // [fsm_arc t.forced_u.state::S1->S2]
%000001 // [fsm_state t.forced_u.state::S0]
%000002 // [fsm_state t.forced_u.state::S1]
%000002 // [fsm_state t.forced_u.state::S2]
%000000 // [fsm_state t.forced_u.state::S3] *** UNCOVERED ***
2'd0: state <= 2'd1;
2'd1: state <= 2'd2;
default: state <= 2'd0;
endcase
end
end
endmodule
module t (
input logic clk
);
integer cyc;
logic rst;
logic start;
fsm_style_incl style_u (.clk(clk), .rst(rst), .start(start));
fsm_default_incl_ok default_incl_u (.clk(clk), .rst(rst), .start(start));
fsm_forced_ok forced_u (.clk(clk), .rst(rst));
initial begin
cyc = 0;
rst = 1'b1;
start = 1'b0;
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
endmodule

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage forced non-enum test
# DESCRIPTION: Verilator: FSM coverage keeps grouped accepted policy-style forms
#
# 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
@ -17,8 +17,6 @@ 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",

View File

@ -0,0 +1,113 @@
// DESCRIPTION: Verilator: FSM coverage keeps grouped accepted policy-style forms
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Group accepted policy-driven forms that should stay inferred, even though
// they exercise different coverage attributes or non-enum state policies.
module fsm_style_incl (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2,
S3 = 2'd3
} state_t;
state_t state /*verilator fsm_arc_include_cond*/;
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
module fsm_default_incl_ok (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q /*verilator fsm_arc_include_cond*/;
state_t state_d;
always_comb begin
state_d = state_q;
case (state_q)
S0: state_d = start ? S1 : S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module fsm_forced_ok (
input logic clk,
input logic rst
);
logic [1:0] state /*verilator fsm_state*/;
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
module t (
input logic clk
);
integer cyc;
logic rst;
logic start;
fsm_style_incl style_u (.clk(clk), .rst(rst), .start(start));
fsm_default_incl_ok default_incl_u (.clk(clk), .rst(rst), .start(start));
fsm_forced_ok forced_u (.clk(clk), .rst(rst));
initial begin
cyc = 0;
rst = 1'b1;
start = 1'b0;
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
endmodule

View File

@ -1,12 +1,16 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM coverage reset policy test
// DESCRIPTION: Verilator: FSM coverage grouped reset semantics 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
// Group accepted simulator-style reset semantics, including reset include /
// exclude behavior and nearby fallback cases that should still preserve FSMs.
module fsm_reset_policy (
input logic clk,
input logic rst
);
typedef enum logic [0:0] {
@ -14,51 +18,179 @@
S1 = 1'b1
} state_t;
%000001 logic rst;
integer cyc;
%000001 state_t state_incl /*verilator fsm_reset_arc*/;
%000001 state_t state_excl;
state_t state_incl /*verilator fsm_reset_arc*/;
state_t state_excl;
%000001 initial begin
%000001 rst = 1'b1;
%000001 cyc = 0;
always_ff @(posedge clk) begin
if (rst) state_incl <= S0;
else
%000001 case (state_incl)
// [FSM coverage]
%000001 // [fsm_arc t.reset_policy_u.state_incl::ANY->S0[reset_include]] [reset arc, excluded from %]
%000001 // [fsm_arc t.reset_policy_u.state_incl::S0->S1]
%000000 // [fsm_state t.reset_policy_u.state_incl::S0] *** UNCOVERED ***
%000001 // [fsm_state t.reset_policy_u.state_incl::S1]
S0: state_incl <= S1;
default: state_incl <= S1;
endcase
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;
always_ff @(posedge clk) begin
if (rst) state_excl <= S0;
else
%000001 case (state_excl)
// [FSM coverage]
%000001 // [fsm_arc t.reset_policy_u.state_excl::ANY->S0[reset]] [reset arc, excluded from %]
%000001 // [fsm_arc t.reset_policy_u.state_excl::S0->S1]
%000000 // [fsm_state t.reset_policy_u.state_excl::S0] *** UNCOVERED ***
%000001 // [fsm_state t.reset_policy_u.state_excl::S1]
S0: state_excl <= S1;
default: state_excl <= S1;
endcase
end
endmodule
module fsm_reset_other_assign_ok (
input logic clk,
input logic rst
);
typedef enum logic [1:0] {
S0 = 2'b00,
S1 = 2'b01,
S2 = 2'b10
} state_t;
logic aux;
logic start;
state_t state_q;
state_t state_d;
initial begin
aux = 1'b1;
start = 1'b1;
end
always_ff @(posedge clk) begin
if (rst) begin
state_q <= S0;
aux <= 1'b0;
end else begin
state_q <= state_d;
end
end
%000006 always_ff @(posedge clk) begin
%000004 if (rst) state_incl <= S0;
else
%000004 case (state_incl)
always_comb begin
state_d = state_q;
%000002 case (state_q)
// [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
%000001 // [fsm_arc t.reset_other_assign_ok_u.state_q::ANY->S0[reset]] [reset arc, excluded from %]
%000002 // [fsm_arc t.reset_other_assign_ok_u.state_q::S0->S1]
%000000 // [fsm_arc t.reset_other_assign_ok_u.state_q::S0->S2]
%000002 // [fsm_state t.reset_other_assign_ok_u.state_q::S0]
%000002 // [fsm_state t.reset_other_assign_ok_u.state_q::S1]
%000000 // [fsm_state t.reset_other_assign_ok_u.state_q::S2] *** UNCOVERED ***
S0: state_d = start ? S1 : S2;
default: state_d = S0;
endcase
end
endmodule
module fsm_oneblock_reset_mismatch_ok (
input logic clk,
input logic rst
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q;
state_t other_q;
initial begin
state_q = S0;
other_q = S2;
end
%000006 always_ff @(posedge clk) begin
%000004 if (rst) state_excl <= S0;
else
%000004 case (state_excl)
always_ff @(posedge clk) begin
if (rst) begin
other_q <= S0;
end else begin
%000001 case (state_q)
// [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;
%000001 // [fsm_arc t.oneblock_reset_mismatch_ok_u.state_q::S0->S1]
%000000 // [fsm_state t.oneblock_reset_mismatch_ok_u.state_q::S0] *** UNCOVERED ***
%000001 // [fsm_state t.oneblock_reset_mismatch_ok_u.state_q::S1]
%000001 // [fsm_state t.oneblock_reset_mismatch_ok_u.state_q::S2]
S0: state_q <= S1;
default: state_q <= S2;
endcase
end
end
endmodule
module fsm_oneblock_reset_nonconst_ok (
input logic clk,
input logic rst
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q;
state_t other_q;
initial begin
state_q = S0;
other_q = S2;
end
// Useful because the one-block reset fallback must ignore non-constant reset
// assignments and still retain the supported case(state_q) FSM below.
always_ff @(posedge clk) begin
if (rst) begin
state_q <= other_q;
end else begin
%000001 case (state_q)
// [FSM coverage]
%000000 // [fsm_arc t.oneblock_reset_nonconst_ok_u.state_q::S0->S1]
%000000 // [fsm_state t.oneblock_reset_nonconst_ok_u.state_q::S0] *** UNCOVERED ***
%000000 // [fsm_state t.oneblock_reset_nonconst_ok_u.state_q::S1] *** UNCOVERED ***
%000001 // [fsm_state t.oneblock_reset_nonconst_ok_u.state_q::S2]
S0: state_q <= S1;
default: state_q <= S2;
endcase
end
end
endmodule
module t (
input logic clk
);
logic rst;
integer cyc;
fsm_reset_policy reset_policy_u (.clk(clk), .rst(rst));
fsm_reset_other_assign_ok reset_other_assign_ok_u (.clk(clk), .rst(rst));
fsm_oneblock_reset_mismatch_ok oneblock_reset_mismatch_ok_u (.clk(clk), .rst(rst));
fsm_oneblock_reset_nonconst_ok oneblock_reset_nonconst_ok_u (.clk(clk), .rst(rst));
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
endmodule

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage reset policy test
# DESCRIPTION: Verilator: FSM coverage grouped reset semantics 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
@ -13,21 +13,10 @@ import vltest_bootstrap
test.scenarios('simulator')
test.compile(verilator_flags2=['--cc --coverage'])
test.compile(verilator_flags2=['--cc --coverage-fsm'])
test.execute()
test.run(cmd=[
os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
"--include-reset-arcs",
test.obj_dir + "/coverage.dat",
],
logfile=test.obj_dir + "/summary.log",
tee=False,
verilator_run=True)
test.files_identical(test.obj_dir + "/summary.log", "t/" + test.name + "_summary.out")
test.run(cmd=[
os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage",
"--annotate",
@ -36,6 +25,17 @@ test.run(cmd=[
],
verilator_run=True)
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)
test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename)
test.files_identical(test.obj_dir + "/annotated-incl/" + test.name + ".v",
"t/" + test.name + "_incl.out")
test.passes()

View File

@ -1,11 +1,15 @@
// DESCRIPTION: Verilator: FSM coverage reset policy test
// DESCRIPTION: Verilator: FSM coverage grouped reset semantics 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
// Group accepted simulator-style reset semantics, including reset include /
// exclude behavior and nearby fallback cases that should still preserve FSMs.
module fsm_reset_policy (
input logic clk,
input logic rst
);
typedef enum logic [0:0] {
@ -13,25 +17,9 @@ module t (
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
@ -49,5 +37,132 @@ module t (
default: state_excl <= S1;
endcase
end
endmodule
module fsm_reset_other_assign_ok (
input logic clk,
input logic rst
);
typedef enum logic [1:0] {
S0 = 2'b00,
S1 = 2'b01,
S2 = 2'b10
} state_t;
logic aux;
logic start;
state_t state_q;
state_t state_d;
initial begin
aux = 1'b1;
start = 1'b1;
end
always_ff @(posedge clk) begin
if (rst) begin
state_q <= S0;
aux <= 1'b0;
end else begin
state_q <= state_d;
end
end
always_comb begin
state_d = state_q;
case (state_q)
S0: state_d = start ? S1 : S2;
default: state_d = S0;
endcase
end
endmodule
module fsm_oneblock_reset_mismatch_ok (
input logic clk,
input logic rst
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q;
state_t other_q;
initial begin
state_q = S0;
other_q = S2;
end
always_ff @(posedge clk) begin
if (rst) begin
other_q <= S0;
end else begin
case (state_q)
S0: state_q <= S1;
default: state_q <= S2;
endcase
end
end
endmodule
module fsm_oneblock_reset_nonconst_ok (
input logic clk,
input logic rst
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q;
state_t other_q;
initial begin
state_q = S0;
other_q = S2;
end
// Useful because the one-block reset fallback must ignore non-constant reset
// assignments and still retain the supported case(state_q) FSM below.
always_ff @(posedge clk) begin
if (rst) begin
state_q <= other_q;
end else begin
case (state_q)
S0: state_q <= S1;
default: state_q <= S2;
endcase
end
end
endmodule
module t (
input logic clk
);
logic rst;
integer cyc;
fsm_reset_policy reset_policy_u (.clk(clk), .rst(rst));
fsm_reset_other_assign_ok reset_other_assign_ok_u (.clk(clk), .rst(rst));
fsm_oneblock_reset_mismatch_ok oneblock_reset_mismatch_ok_u (.clk(clk), .rst(rst));
fsm_oneblock_reset_nonconst_ok oneblock_reset_nonconst_ok_u (.clk(clk), .rst(rst));
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
endmodule

View File

@ -0,0 +1,197 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM coverage grouped reset semantics test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Group accepted simulator-style reset semantics, including reset include /
// exclude behavior and nearby fallback cases that should still preserve FSMs.
module fsm_reset_policy (
input logic clk,
input logic rst
);
typedef enum logic [0:0] {
S0 = 1'b0,
S1 = 1'b1
} state_t;
state_t state_incl /*verilator fsm_reset_arc*/;
state_t state_excl;
always_ff @(posedge clk) begin
if (rst) state_incl <= S0;
else
%000001 case (state_incl)
// [FSM coverage]
%000001 // [fsm_arc t.reset_policy_u.state_incl::ANY->S0[reset_include]]
%000001 // [fsm_arc t.reset_policy_u.state_incl::S0->S1]
%000000 // [fsm_state t.reset_policy_u.state_incl::S0] *** UNCOVERED ***
%000001 // [fsm_state t.reset_policy_u.state_incl::S1]
S0: state_incl <= S1;
default: state_incl <= S1;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_excl <= S0;
else
%000001 case (state_excl)
// [FSM coverage]
%000001 // [fsm_arc t.reset_policy_u.state_excl::ANY->S0[reset]]
%000001 // [fsm_arc t.reset_policy_u.state_excl::S0->S1]
%000000 // [fsm_state t.reset_policy_u.state_excl::S0] *** UNCOVERED ***
%000001 // [fsm_state t.reset_policy_u.state_excl::S1]
S0: state_excl <= S1;
default: state_excl <= S1;
endcase
end
endmodule
module fsm_reset_other_assign_ok (
input logic clk,
input logic rst
);
typedef enum logic [1:0] {
S0 = 2'b00,
S1 = 2'b01,
S2 = 2'b10
} state_t;
logic aux;
logic start;
state_t state_q;
state_t state_d;
initial begin
aux = 1'b1;
start = 1'b1;
end
always_ff @(posedge clk) begin
if (rst) begin
state_q <= S0;
aux <= 1'b0;
end else begin
state_q <= state_d;
end
end
always_comb begin
state_d = state_q;
%000002 case (state_q)
// [FSM coverage]
%000001 // [fsm_arc t.reset_other_assign_ok_u.state_q::ANY->S0[reset]]
%000002 // [fsm_arc t.reset_other_assign_ok_u.state_q::S0->S1]
%000000 // [fsm_arc t.reset_other_assign_ok_u.state_q::S0->S2]
%000002 // [fsm_state t.reset_other_assign_ok_u.state_q::S0]
%000002 // [fsm_state t.reset_other_assign_ok_u.state_q::S1]
%000000 // [fsm_state t.reset_other_assign_ok_u.state_q::S2] *** UNCOVERED ***
S0: state_d = start ? S1 : S2;
default: state_d = S0;
endcase
end
endmodule
module fsm_oneblock_reset_mismatch_ok (
input logic clk,
input logic rst
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q;
state_t other_q;
initial begin
state_q = S0;
other_q = S2;
end
always_ff @(posedge clk) begin
if (rst) begin
other_q <= S0;
end else begin
%000001 case (state_q)
// [FSM coverage]
%000001 // [fsm_arc t.oneblock_reset_mismatch_ok_u.state_q::S0->S1]
%000000 // [fsm_state t.oneblock_reset_mismatch_ok_u.state_q::S0] *** UNCOVERED ***
%000001 // [fsm_state t.oneblock_reset_mismatch_ok_u.state_q::S1]
%000001 // [fsm_state t.oneblock_reset_mismatch_ok_u.state_q::S2]
S0: state_q <= S1;
default: state_q <= S2;
endcase
end
end
endmodule
module fsm_oneblock_reset_nonconst_ok (
input logic clk,
input logic rst
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q;
state_t other_q;
initial begin
state_q = S0;
other_q = S2;
end
// Useful because the one-block reset fallback must ignore non-constant reset
// assignments and still retain the supported case(state_q) FSM below.
always_ff @(posedge clk) begin
if (rst) begin
state_q <= other_q;
end else begin
%000001 case (state_q)
// [FSM coverage]
%000000 // [fsm_arc t.oneblock_reset_nonconst_ok_u.state_q::S0->S1]
%000000 // [fsm_state t.oneblock_reset_nonconst_ok_u.state_q::S0] *** UNCOVERED ***
%000000 // [fsm_state t.oneblock_reset_nonconst_ok_u.state_q::S1] *** UNCOVERED ***
%000001 // [fsm_state t.oneblock_reset_nonconst_ok_u.state_q::S2]
S0: state_q <= S1;
default: state_q <= S2;
endcase
end
end
endmodule
module t (
input logic clk
);
logic rst;
integer cyc;
fsm_reset_policy reset_policy_u (.clk(clk), .rst(rst));
fsm_reset_other_assign_ok reset_other_assign_ok_u (.clk(clk), .rst(rst));
fsm_oneblock_reset_mismatch_ok oneblock_reset_mismatch_ok_u (.clk(clk), .rst(rst));
fsm_oneblock_reset_nonconst_ok oneblock_reset_nonconst_ok_u (.clk(clk), .rst(rst));
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
endmodule

View File

@ -1,63 +1,6 @@
// // 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
%Warning-COVERIGN: t/t_cover_fsm_reset_multi.v:41:13: Ignoring unsupported: FSM coverage on reset branches with multiple assignments to the state variable
41 | state <= S1;
| ^~
... 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

@ -1,5 +1,5 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage reset pseudo-vertex reuse test
# DESCRIPTION: Verilator: FSM coverage multi-reset assignment 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
@ -7,30 +7,14 @@
# 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.scenarios('vlt')
# 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)
# Multiple reset assignments to the same FSM state variable are legal RTL but
# not a realistic reset style to model as distinct reset arcs. Warn and ignore
# reset-arc extraction for that branch instead of inventing multiple ANY->state
# coverage edges.
test.lint(verilator_flags2=["--coverage-fsm"], fails=True, expect_filename=test.golden_filename)
test.passes()

View File

@ -1,4 +1,4 @@
// DESCRIPTION: Verilator: FSM coverage reset pseudo-vertex reuse test
// DESCRIPTION: Verilator: FSM coverage multi-reset assignment warning test
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
@ -32,10 +32,9 @@ module t (
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.
// This reset block is intentionally non-idiomatic. Multiple assignments to
// the FSM state variable in the reset branch should warn and be ignored for
// reset-arc extraction rather than inventing multiple reset transitions.
always_ff @(posedge clk) begin
if (rst) begin
state <= S0;

View File

@ -1,7 +0,0 @@
Coverage Summary:
line : 100.0% (8/8)
toggle : 75.0% (6/8)
branch : 100.0% (8/8)
expr : 0.0% (0/0)
fsm_state : 50.0% (2/4)
fsm_arc : 100.0% (4/4)

View File

@ -1,67 +0,0 @@
// // 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

@ -1,55 +0,0 @@
// 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 @@
# SystemC::Coverage-3

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage style coverage test
# DESCRIPTION: Verilator: FSM coverage ignores grouped unsupported FSM extraction patterns
#
# 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
@ -7,8 +7,6 @@
# SPDX-FileCopyrightText: 2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import os
import vltest_bootstrap
test.scenarios('simulator')
@ -17,11 +15,6 @@ 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.files_identical(test.obj_dir + "/coverage.dat", "t/" + test.name + ".out")
test.passes()

View File

@ -0,0 +1,819 @@
// DESCRIPTION: Verilator: FSM coverage ignores grouped unsupported FSM extraction patterns
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Group unsupported extraction shapes that must compile and run cleanly while
// emitting no FSM coverage points at all.
module fsm_caseitem_varrhs_bad (
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
logic start;
integer cyc;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
state_t other_d;
initial begin
start = 1'b0;
cyc = 0;
other_d = S1;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 2) start <= 1'b1;
if (cyc == 3) start <= 1'b0;
end
always_comb begin
state_d = state_q;
case (state_q)
S0: state_d = other_d;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
endmodule
module fsm_caseitem_then_nonconst_bad (
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1
} state_t;
logic sel;
integer cyc;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
state_t other_d;
initial begin
sel = 1'b0;
cyc = 0;
other_d = S1;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 2) sel <= 1'b1;
if (cyc == 3) sel <= 1'b0;
end
always_comb begin
state_d = state_q;
/* verilator lint_off CASEINCOMPLETE */
case (state_q)
S0: state_d = sel ? other_d : S1;
endcase
/* verilator lint_on CASEINCOMPLETE */
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
endmodule
module fsm_caseitem_else_nonconst_bad (
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1
} state_t;
logic sel;
integer cyc;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
state_t other_d;
initial begin
sel = 1'b0;
cyc = 0;
other_d = S1;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 2) sel <= 1'b1;
if (cyc == 3) sel <= 1'b0;
end
always_comb begin
state_d = state_q;
/* verilator lint_off CASEINCOMPLETE */
case (state_q)
S0: state_d = sel ? S1 : other_d;
endcase
/* verilator lint_on CASEINCOMPLETE */
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
endmodule
module fsm_oneblock_then_bad (
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
logic rst;
logic sel;
integer cyc;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
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;
end
always_comb begin
state_d = state_q;
case (state_q)
S0: state_d = sel ? S1 : S0;
S1: state_d = S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= rst ? (sel ? S0 : S1) : state_d;
end
endmodule
module fsm_oneblock_else_bad (
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
logic rst;
logic sel;
integer cyc;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
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;
end
always_comb begin
state_d = state_q;
case (state_q)
S0: state_d = sel ? S1 : S0;
S1: state_d = S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= rst ? S0 : (sel ? S1 : state_d);
end
endmodule
module fsm_combo_sel_expr_bad (
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
logic rst;
logic start;
integer cyc;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
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;
end
always_comb begin
state_d = state_q;
case (state_q + 1)
S0: state_d = start ? S1 : S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) begin
state_q <= S0;
end else begin
state_q <= state_d;
end
end
endmodule
module fsm_normalized_if_noelse_bad (
input logic clk
);
typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t;
logic rst;
logic start;
integer cyc;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
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;
end
always_comb begin
state_d = state_q;
case (state_q)
S0: if (start) ; else state_d = S1;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module fsm_normalized_if_then_nonconst_bad (
input logic clk
);
typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t;
logic sel;
integer cyc;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
state_t tmp_a;
state_t tmp_b;
state_t other_d;
initial begin
sel = 1'b0;
cyc = 0;
other_d = S2;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 2) sel <= 1'b1;
if (cyc == 3) sel <= 1'b0;
end
always_comb begin
state_d = state_q;
case (state_q)
S0: begin
if (sel) tmp_a = other_d;
else tmp_b = S1;
state_d = tmp_a;
end
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
endmodule
module fsm_normalized_if_else_nonconst_bad (
input logic clk
);
typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t;
logic sel;
integer cyc;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
state_t tmp_a;
state_t tmp_b;
state_t other_d;
initial begin
sel = 1'b0;
cyc = 0;
other_d = S2;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 2) sel <= 1'b1;
if (cyc == 3) sel <= 1'b0;
end
always_comb begin
state_d = state_q;
case (state_q)
S0: begin
if (sel) tmp_a = S1;
else tmp_b = other_d;
state_d = tmp_a;
end
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
endmodule
module fsm_normalized_if_temp_mismatch_bad (
input logic clk
);
typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t;
logic sel;
integer cyc;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
state_t tmp_a;
state_t tmp_b;
initial begin
sel = 1'b0;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 2) sel <= 1'b1;
if (cyc == 3) sel <= 1'b0;
end
always_comb begin
state_d = state_q;
case (state_q)
S0: begin
if (sel) tmp_a = S1;
else tmp_b = S2;
state_d = tmp_a;
end
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
endmodule
module fsm_normalized_if_then_state_else_other_bad (
input logic clk
);
typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t;
logic sel;
integer cyc;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
state_t tmp_a;
initial begin
sel = 1'b0;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 2) sel <= 1'b1;
if (cyc == 3) sel <= 1'b0;
end
always_comb begin
state_d = state_q;
case (state_q)
S0: begin
if (sel) state_d = S1;
else tmp_a = S2;
state_d = tmp_a;
end
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
endmodule
module fsm_normalized_if_same_temp_nofollow_bad (
input logic clk
);
typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t;
logic sel;
logic aux;
integer cyc;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
state_t tmp_a;
initial begin
sel = 1'b0;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 2) sel <= 1'b1;
if (cyc == 3) sel <= 1'b0;
end
always_comb begin
state_d = state_q;
case (state_q)
S0: begin
if (sel) begin
tmp_a = S1;
aux = 1'b1;
end else begin
tmp_a = S2;
aux = 1'b0;
end
end
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
endmodule
module fsm_normalized_if_follow_nonvar_bad (
input logic clk
);
typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t;
logic sel;
logic aux;
integer cyc;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
state_t tmp_a;
initial begin
sel = 1'b0;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 2) sel <= 1'b1;
if (cyc == 3) sel <= 1'b0;
end
always_comb begin
state_d = state_q;
case (state_q)
S0: begin
if (sel) begin
tmp_a = S1;
aux = 1'b1;
end else begin
tmp_a = S2;
aux = 1'b0;
end
// Final follow-up is not a plain var-to-var state assignment.
aux = (tmp_a == S1);
end
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
endmodule
module fsm_normalized_if_follow_wrongfrom_bad (
input logic clk
);
typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t;
logic sel;
logic aux;
integer cyc;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
state_t tmp_a;
state_t other_d;
initial begin
sel = 1'b0;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 2) sel <= 1'b1;
if (cyc == 3) sel <= 1'b0;
end
always_comb begin
state_d = state_q;
case (state_q)
S0: begin
if (sel) begin
tmp_a = S1;
aux = 1'b1;
end else begin
tmp_a = S2;
aux = 1'b0;
end
// Final follow-up reads from the wrong source temp.
state_d = other_d;
end
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
endmodule
module fsm_normalized_if_follow_wronglhs_bad (
input logic clk
);
typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t;
logic sel;
logic aux;
integer cyc;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
state_t tmp_a;
state_t other_d;
initial begin
sel = 1'b0;
cyc = 0;
end
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 2) sel <= 1'b1;
if (cyc == 3) sel <= 1'b0;
end
always_comb begin
state_d = state_q;
case (state_q)
S0: begin
if (sel) begin
tmp_a = S1;
aux = 1'b1;
end else begin
tmp_a = S2;
aux = 1'b0;
end
// Final follow-up writes the wrong lhs instead of state_d.
other_d = tmp_a;
end
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
endmodule
module fsm_case_next_wrongrhs_bad (
input logic clk
);
typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t;
logic rst;
logic start;
integer cyc;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
state_t other_d;
initial begin
rst = 1'b1;
start = 1'b0;
cyc = 0;
other_d = S1;
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;
end
always_comb begin
state_d = other_d;
case (state_d)
S0: state_d = start ? S1 : S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module fsm_forced_wide_bad (
input logic clk
);
integer cyc;
logic rst;
logic [30: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 <= 31'd0;
end else begin
case (state)
31'd0: state <= 31'd1;
31'd1: state <= 31'd2;
default: state <= 31'd0;
endcase
end
end
endmodule
module fsm_reset_commit_mismatch_bad (
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
logic rst;
logic start;
integer cyc;
state_t state_q /*verilator fsm_state*/;
state_t other_q;
state_t state_d;
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_comb begin
state_d = state_q;
case (state_q)
S0: state_d = start ? S1 : S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) other_q <= S0;
else state_q <= state_d;
end
endmodule
module fsm_reset_then_bad (
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
logic rst;
logic sel;
integer cyc;
state_t state_q /*verilator fsm_state*/;
state_t state_d;
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 == 8) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
always_comb begin
state_d = state_q;
case (state_q)
S0: state_d = sel ? S1 : S0;
S1: state_d = S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= sel ? S0 : S1;
else state_q <= state_d;
end
endmodule
module t (
input logic clk
);
integer cyc;
fsm_caseitem_varrhs_bad caseitem_varrhs_bad_u (.clk(clk));
fsm_caseitem_then_nonconst_bad caseitem_then_nonconst_bad_u (.clk(clk));
fsm_caseitem_else_nonconst_bad caseitem_else_nonconst_bad_u (.clk(clk));
fsm_oneblock_then_bad oneblock_then_bad_u (.clk(clk));
fsm_oneblock_else_bad oneblock_else_bad_u (.clk(clk));
fsm_combo_sel_expr_bad combo_sel_expr_bad_u (.clk(clk));
fsm_normalized_if_noelse_bad normalized_if_noelse_bad_u (.clk(clk));
fsm_normalized_if_then_nonconst_bad normalized_if_then_nonconst_bad_u (.clk(clk));
fsm_normalized_if_else_nonconst_bad normalized_if_else_nonconst_bad_u (.clk(clk));
fsm_normalized_if_temp_mismatch_bad normalized_if_temp_mismatch_bad_u (.clk(clk));
fsm_normalized_if_then_state_else_other_bad normalized_if_then_state_else_other_bad_u (.clk(clk));
fsm_normalized_if_same_temp_nofollow_bad normalized_if_same_temp_nofollow_bad_u (.clk(clk));
fsm_normalized_if_follow_nonvar_bad normalized_if_follow_nonvar_bad_u (.clk(clk));
fsm_normalized_if_follow_wrongfrom_bad normalized_if_follow_wrongfrom_bad_u (.clk(clk));
fsm_normalized_if_follow_wronglhs_bad normalized_if_follow_wronglhs_bad_u (.clk(clk));
fsm_case_next_wrongrhs_bad case_next_wrongrhs_bad_u (.clk(clk));
fsm_forced_wide_bad forced_wide_bad_u (.clk(clk));
fsm_reset_commit_mismatch_bad reset_commit_mismatch_bad_u (.clk(clk));
fsm_reset_then_bad reset_then_bad_u (.clk(clk));
initial cyc = 0;
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 8) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
endmodule

View File

@ -0,0 +1,459 @@
// // verilator_coverage annotation
// DESCRIPTION: Verilator: FSM coverage combined two-process/three-block regression
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Group supported multi-process and nearby supported-core variants so the
// main two-process / three-block extraction family stays in one place.
module fsm_basic (
%000009 input logic clk,
%000001 input logic rst,
%000001 input logic start
);
typedef enum logic [1:0] {
S_IDLE = 2'd0,
S_RUN = 2'd1,
S_DONE = 2'd2,
S_ERR = 2'd3
} state_t;
%000002 state_t state_q /*verilator fsm_reset_arc*/;
%000002 state_t state_d;
000010 always_comb begin
000010 state_d = state_q;
~000010 case (state_q)
// [FSM coverage]
%000001 // [fsm_arc t.basic_u.state_q::ANY->S_IDLE[reset_include]] [reset arc, excluded from %]
%000001 // [fsm_arc t.basic_u.state_q::S_RUN->S_DONE]
%000001 // [fsm_state t.basic_u.state_q::S_DONE]
%000001 // [fsm_state t.basic_u.state_q::S_ERR]
%000000 // [fsm_state t.basic_u.state_q::S_IDLE] *** UNCOVERED ***
%000001 // [fsm_state t.basic_u.state_q::S_RUN]
%000004 S_IDLE: if (start) state_d = S_RUN;
%000001 S_RUN: state_d = S_DONE;
%000005 default: state_d = S_ERR;
endcase
end
%000009 always_ff @(posedge clk) begin
%000007 if (rst) state_q <= S_IDLE;
%000007 else state_q <= state_d;
end
endmodule
module fsm_three_block (
%000009 input logic clk,
%000001 input logic rst,
%000001 input logic start
);
typedef enum logic [1:0] {
S_IDLE = 2'd0,
S_BUSY = 2'd1,
S_DONE = 2'd2
} state_t;
%000001 logic busy;
%000001 logic done;
%000001 state_t state_q;
%000001 state_t state_d;
000010 always_comb begin
000010 state_d = state_q;
~000010 case (state_q)
// [FSM coverage]
%000001 // [fsm_arc t.three_block_u.state_q::ANY->S_IDLE[reset]] [reset arc, excluded from %]
%000001 // [fsm_arc t.three_block_u.state_q::S_BUSY->S_DONE]
%000001 // [fsm_arc t.three_block_u.state_q::S_DONE->S_IDLE]
%000001 // [fsm_state t.three_block_u.state_q::S_BUSY]
%000001 // [fsm_state t.three_block_u.state_q::S_DONE]
%000001 // [fsm_state t.three_block_u.state_q::S_IDLE]
%000008 S_IDLE: if (start) state_d = S_BUSY;
%000001 S_BUSY: state_d = S_DONE;
%000001 S_DONE: state_d = S_IDLE;
%000000 default: state_d = S_IDLE;
endcase
end
%000001 always_comb begin
%000001 busy = (state_q == S_BUSY);
%000001 done = (state_q == S_DONE);
end
%000009 always_ff @(posedge clk) begin
%000007 if (rst) state_q <= S_IDLE;
%000007 else state_q <= state_d;
end
endmodule
module fsm_mealy (
%000009 input logic clk,
%000001 input logic rst,
%000001 input logic start,
%000001 input logic bit_done
);
typedef enum logic [1:0] {
S_IDLE = 2'd0,
S_SEND = 2'd1,
S_DONE = 2'd2
} state_t;
%000002 logic tx;
%000001 state_t state_q;
%000001 state_t state_d;
000010 always_comb begin
000010 state_d = state_q;
000010 tx = 1'b1;
~000010 case (state_q)
// [FSM coverage]
%000001 // [fsm_arc t.mealy_u.state_q::ANY->S_IDLE[reset]] [reset arc, excluded from %]
%000001 // [fsm_arc t.mealy_u.state_q::S_DONE->S_IDLE]
%000001 // [fsm_state t.mealy_u.state_q::S_DONE]
%000001 // [fsm_state t.mealy_u.state_q::S_IDLE]
%000001 // [fsm_state t.mealy_u.state_q::S_SEND]
%000007 S_IDLE: begin
%000006 if (start) begin
%000001 state_d = S_SEND;
%000001 tx = 1'b0;
end
end
%000002 S_SEND: begin
%000002 tx = bit_done;
%000001 if (bit_done) state_d = S_DONE;
end
%000001 S_DONE: state_d = S_IDLE;
%000000 default: state_d = S_IDLE;
endcase
end
%000009 always_ff @(posedge clk) begin
%000007 if (rst) state_q <= S_IDLE;
%000007 else state_q <= state_d;
end
endmodule
module fsm_reset_policy (
%000009 input logic clk,
%000001 input logic rst
);
typedef enum logic [0:0] {
S0 = 1'b0,
S1 = 1'b1
} state_t;
%000001 state_t state_incl_q /*verilator fsm_reset_arc*/;
%000001 state_t state_incl_d;
%000001 state_t state_excl_q;
%000001 state_t state_excl_d;
000010 always_comb begin
000010 state_incl_d = state_incl_q;
~000010 case (state_incl_q)
// [FSM coverage]
%000001 // [fsm_arc t.reset_u.state_incl_q::ANY->S0[reset_include]] [reset arc, excluded from %]
%000001 // [fsm_arc t.reset_u.state_incl_q::S0->S1]
%000000 // [fsm_state t.reset_u.state_incl_q::S0] *** UNCOVERED ***
%000001 // [fsm_state t.reset_u.state_incl_q::S1]
%000003 S0: state_incl_d = S1;
%000007 default: state_incl_d = S1;
endcase
end
000010 always_comb begin
000010 state_excl_d = state_excl_q;
~000010 case (state_excl_q)
// [FSM coverage]
%000001 // [fsm_arc t.reset_u.state_excl_q::ANY->S0[reset]] [reset arc, excluded from %]
%000001 // [fsm_arc t.reset_u.state_excl_q::S0->S1]
%000000 // [fsm_state t.reset_u.state_excl_q::S0] *** UNCOVERED ***
%000001 // [fsm_state t.reset_u.state_excl_q::S1]
%000003 S0: state_excl_d = S1;
%000007 default: state_excl_d = S1;
endcase
end
%000009 always_ff @(posedge clk) begin
%000007 if (rst) state_incl_q <= S0;
%000007 else state_incl_q <= state_incl_d;
end
%000009 always_ff @(posedge clk) begin
%000007 if (rst) state_excl_q <= S0;
%000007 else state_excl_q <= state_excl_d;
end
endmodule
module fsm_nextstate_sel_off (
%000009 input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1
} state_t;
%000001 state_t state_q;
%000001 state_t state_d;
%000001 always_comb begin
%000001 state_d = S0;
%000001 case (state_d)
%000001 S0: state_d = S1;
%000000 default: state_d = S0;
endcase
end
%000009 always_ff @(posedge clk) begin
%000009 state_q <= state_d;
end
endmodule
module fsm_nextstate_sel_ok (
%000009 input logic clk,
%000001 input logic rst,
%000001 input logic start
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
%000001 state_t state_q;
%000001 state_t state_d;
000010 always_comb begin
000010 state_d = state_q;
~000010 case (state_d)
// [FSM coverage]
%000001 // [fsm_arc t.nextstate_sel_ok_u.state_q::ANY->S0[reset]] [reset arc, excluded from %]
%000001 // [fsm_arc t.nextstate_sel_ok_u.state_q::S1->S2]
%000001 // [fsm_state t.nextstate_sel_ok_u.state_q::S0]
%000001 // [fsm_state t.nextstate_sel_ok_u.state_q::S1]
%000001 // [fsm_state t.nextstate_sel_ok_u.state_q::S2]
%000008 S0: if (start) state_d = S1;
%000001 S1: state_d = S2;
%000001 default: state_d = S0;
endcase
end
%000009 always_ff @(posedge clk) begin
%000007 if (rst) state_q <= S0;
%000007 else state_q <= state_d;
end
endmodule
module fsm_ternary (
%000009 input logic clk,
%000001 input logic rst,
%000001 input logic sel
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
%000004 state_t state_q;
%000004 state_t state_d;
000010 always_comb begin
000010 state_d = state_q;
~000010 case (state_q)
// [FSM coverage]
%000001 // [fsm_arc t.ternary_u.state_q::ANY->S0[reset]] [reset arc, excluded from %]
%000000 // [fsm_arc t.ternary_u.state_q::S0->S1]
%000004 // [fsm_arc t.ternary_u.state_q::S0->S2]
%000003 // [fsm_state t.ternary_u.state_q::S0]
%000000 // [fsm_state t.ternary_u.state_q::S1] *** UNCOVERED ***
%000004 // [fsm_state t.ternary_u.state_q::S2]
%000009 S0: state_d = sel ? S1 : S2;
%000004 default: state_d = S0;
endcase
end
%000009 always_ff @(posedge clk) begin
%000007 if (rst) state_q <= S0;
%000007 else state_q <= state_d;
end
endmodule
module fsm_plain_always (
%000009 input logic clk,
%000001 input logic rst,
%000001 input logic go
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
%000001 state_t state_q;
%000001 state_t state_d;
000010 always @* begin
000010 state_d = state_q;
~000010 case (state_q)
// [FSM coverage]
%000001 // [fsm_arc t.plain_always_u.state_q::ANY->S0[reset]] [reset arc, excluded from %]
%000001 // [fsm_arc t.plain_always_u.state_q::S1->S2]
%000001 // [fsm_state t.plain_always_u.state_q::S0]
%000001 // [fsm_state t.plain_always_u.state_q::S1]
%000001 // [fsm_state t.plain_always_u.state_q::S2]
%000008 S0: if (go) state_d = S1;
%000001 S1: state_d = S2;
%000001 default: state_d = S0;
endcase
end
%000009 always_ff @(posedge clk) begin
%000007 if (rst) state_q <= S0;
%000007 else state_q <= state_d;
end
endmodule
module fsm_plain_always_list (
%000009 input logic clk,
%000001 input logic rst,
%000001 input logic go
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
%000004 state_t state_q;
%000004 state_t state_d;
%000008 always @(state_q or go) begin
%000008 state_d = state_q;
%000008 case (state_q)
// [FSM coverage]
%000001 // [fsm_arc t.plain_always_list_u.state_q::ANY->S0[reset]] [reset arc, excluded from %]
%000000 // [fsm_arc t.plain_always_list_u.state_q::S0->S1]
%000004 // [fsm_arc t.plain_always_list_u.state_q::S0->S2]
%000003 // [fsm_state t.plain_always_list_u.state_q::S0]
%000000 // [fsm_state t.plain_always_list_u.state_q::S1] *** UNCOVERED ***
%000004 // [fsm_state t.plain_always_list_u.state_q::S2]
%000007 S0: state_d = go ? S1 : S2;
%000004 default: state_d = S0;
endcase
end
%000009 always_ff @(posedge clk) begin
%000007 if (rst) state_q <= S0;
%000007 else state_q <= state_d;
end
endmodule
module fsm_caseassigns_off (
%000009 input logic clk,
%000001 input logic rst,
%000001 input logic go
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1
} state_t;
%000001 logic out;
%000000 state_t state_q;
%000000 state_t state_d;
000010 always_comb begin
000010 state_d = state_q;
000010 out = 1'b0;
000010 case (state_q)
000010 S0: out = go;
%000000 default: out = 1'b0;
endcase
end
%000009 always_ff @(posedge clk) begin
%000007 if (rst) state_q <= S0;
%000007 else state_q <= state_d;
end
endmodule
module fsm_seqmix_off (
%000009 input logic clk,
%000001 input logic rst
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1
} state_t;
%000004 logic side;
%000004 state_t state_q;
%000004 state_t state_d;
%000001 initial side = 1'b0;
000010 always_comb begin
000010 state_d = state_q;
000010 case (state_q)
%000006 S0: state_d = S1;
%000004 default: state_d = S0;
endcase
end
%000009 always_ff @(posedge clk) begin
%000007 if (rst) state_q <= S0;
%000007 else begin
%000007 side <= ~side;
%000007 state_q <= state_d;
end
end
endmodule
module t (
%000009 input clk
);
integer cyc;
%000001 logic rst;
%000001 logic start;
%000001 logic bit_done;
%000001 logic sel;
fsm_basic basic_u (.clk(clk), .rst(rst), .start(start));
fsm_three_block three_block_u (.clk(clk), .rst(rst), .start(start));
fsm_mealy mealy_u (.clk(clk), .rst(rst), .start(start), .bit_done(bit_done));
fsm_reset_policy reset_u (.clk(clk), .rst(rst));
fsm_nextstate_sel_off nextstate_sel_off_u (.clk(clk));
fsm_nextstate_sel_ok nextstate_sel_ok_u (.clk(clk), .rst(rst), .start(start));
fsm_ternary ternary_u (.clk(clk), .rst(rst), .sel(sel));
fsm_plain_always plain_always_u (.clk(clk), .rst(rst), .go(start));
fsm_plain_always_list plain_always_list_u (.clk(clk), .rst(rst), .go(start));
fsm_caseassigns_off caseassigns_off_u (.clk(clk), .rst(rst), .go(start));
fsm_seqmix_off seqmix_off_u (.clk(clk), .rst(rst));
%000001 initial begin
%000001 cyc = 0;
%000001 rst = 1'b1;
%000001 start = 1'b0;
%000001 bit_done = 1'b0;
%000001 sel = 1'b0;
end
%000009 always @(posedge clk) begin
%000009 cyc <= cyc + 1;
%000008 if (cyc == 1) rst <= 1'b0;
%000008 if (cyc == 2) start <= 1'b1;
%000008 if (cyc == 3) start <= 1'b0;
%000008 if (cyc == 4) bit_done <= 1'b1;
%000008 if (cyc == 4) sel <= 1'b1;
%000008 if (cyc == 5) bit_done <= 1'b0;
%000008 if (cyc == 5) sel <= 1'b0;
%000008 if (cyc == 8) begin
%000001 $write("*-* All Finished *-*\n");
%000001 $finish;
end
end
endmodule

View File

@ -0,0 +1,33 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: FSM coverage combined two-process/three-block regression
#
# 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",
"--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.file_grep_not(test.obj_dir + "/coverage.dat", r"t\.nextstate_sel_off_u\.state_q.*fsm_")
test.file_grep_not(test.obj_dir + "/coverage.dat", r"t\.caseassigns_off_u\.state_q.*fsm_")
test.file_grep_not(test.obj_dir + "/coverage.dat", r"t\.seqmix_off_u\.state_q.*fsm_")
test.passes()

View File

@ -0,0 +1,401 @@
// DESCRIPTION: Verilator: FSM coverage combined two-process/three-block regression
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Group supported multi-process and nearby supported-core variants so the
// main two-process / three-block extraction family stays in one place.
module fsm_basic (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S_IDLE = 2'd0,
S_RUN = 2'd1,
S_DONE = 2'd2,
S_ERR = 2'd3
} state_t;
state_t state_q /*verilator fsm_reset_arc*/;
state_t state_d;
always_comb begin
state_d = state_q;
case (state_q)
S_IDLE: if (start) state_d = S_RUN;
S_RUN: state_d = S_DONE;
default: state_d = S_ERR;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S_IDLE;
else state_q <= state_d;
end
endmodule
module fsm_three_block (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S_IDLE = 2'd0,
S_BUSY = 2'd1,
S_DONE = 2'd2
} state_t;
logic busy;
logic done;
state_t state_q;
state_t state_d;
always_comb begin
state_d = state_q;
case (state_q)
S_IDLE: if (start) state_d = S_BUSY;
S_BUSY: state_d = S_DONE;
S_DONE: state_d = S_IDLE;
default: state_d = S_IDLE;
endcase
end
always_comb begin
busy = (state_q == S_BUSY);
done = (state_q == S_DONE);
end
always_ff @(posedge clk) begin
if (rst) state_q <= S_IDLE;
else state_q <= state_d;
end
endmodule
module fsm_mealy (
input logic clk,
input logic rst,
input logic start,
input logic bit_done
);
typedef enum logic [1:0] {
S_IDLE = 2'd0,
S_SEND = 2'd1,
S_DONE = 2'd2
} state_t;
logic tx;
state_t state_q;
state_t state_d;
always_comb begin
state_d = state_q;
tx = 1'b1;
case (state_q)
S_IDLE: begin
if (start) begin
state_d = S_SEND;
tx = 1'b0;
end
end
S_SEND: begin
tx = bit_done;
if (bit_done) state_d = S_DONE;
end
S_DONE: state_d = S_IDLE;
default: state_d = S_IDLE;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S_IDLE;
else state_q <= state_d;
end
endmodule
module fsm_reset_policy (
input logic clk,
input logic rst
);
typedef enum logic [0:0] {
S0 = 1'b0,
S1 = 1'b1
} state_t;
state_t state_incl_q /*verilator fsm_reset_arc*/;
state_t state_incl_d;
state_t state_excl_q;
state_t state_excl_d;
always_comb begin
state_incl_d = state_incl_q;
case (state_incl_q)
S0: state_incl_d = S1;
default: state_incl_d = S1;
endcase
end
always_comb begin
state_excl_d = state_excl_q;
case (state_excl_q)
S0: state_excl_d = S1;
default: state_excl_d = S1;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_incl_q <= S0;
else state_incl_q <= state_incl_d;
end
always_ff @(posedge clk) begin
if (rst) state_excl_q <= S0;
else state_excl_q <= state_excl_d;
end
endmodule
module fsm_nextstate_sel_off (
input logic clk
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1
} state_t;
state_t state_q;
state_t state_d;
always_comb begin
state_d = S0;
case (state_d)
S0: state_d = S1;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
endmodule
module fsm_nextstate_sel_ok (
input logic clk,
input logic rst,
input logic start
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q;
state_t state_d;
always_comb begin
state_d = state_q;
case (state_d)
S0: if (start) state_d = S1;
S1: state_d = S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module fsm_ternary (
input logic clk,
input logic rst,
input logic sel
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q;
state_t state_d;
always_comb begin
state_d = state_q;
case (state_q)
S0: state_d = sel ? S1 : S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module fsm_plain_always (
input logic clk,
input logic rst,
input logic go
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q;
state_t state_d;
always @* begin
state_d = state_q;
case (state_q)
S0: if (go) state_d = S1;
S1: state_d = S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module fsm_plain_always_list (
input logic clk,
input logic rst,
input logic go
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1,
S2 = 2'd2
} state_t;
state_t state_q;
state_t state_d;
always @(state_q or go) begin
state_d = state_q;
case (state_q)
S0: state_d = go ? S1 : S2;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module fsm_caseassigns_off (
input logic clk,
input logic rst,
input logic go
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1
} state_t;
logic out;
state_t state_q;
state_t state_d;
always_comb begin
state_d = state_q;
out = 1'b0;
case (state_q)
S0: out = go;
default: out = 1'b0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else state_q <= state_d;
end
endmodule
module fsm_seqmix_off (
input logic clk,
input logic rst
);
typedef enum logic [1:0] {
S0 = 2'd0,
S1 = 2'd1
} state_t;
logic side;
state_t state_q;
state_t state_d;
initial side = 1'b0;
always_comb begin
state_d = state_q;
case (state_q)
S0: state_d = S1;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
if (rst) state_q <= S0;
else begin
side <= ~side;
state_q <= state_d;
end
end
endmodule
module t (
input clk
);
integer cyc;
logic rst;
logic start;
logic bit_done;
logic sel;
fsm_basic basic_u (.clk(clk), .rst(rst), .start(start));
fsm_three_block three_block_u (.clk(clk), .rst(rst), .start(start));
fsm_mealy mealy_u (.clk(clk), .rst(rst), .start(start), .bit_done(bit_done));
fsm_reset_policy reset_u (.clk(clk), .rst(rst));
fsm_nextstate_sel_off nextstate_sel_off_u (.clk(clk));
fsm_nextstate_sel_ok nextstate_sel_ok_u (.clk(clk), .rst(rst), .start(start));
fsm_ternary ternary_u (.clk(clk), .rst(rst), .sel(sel));
fsm_plain_always plain_always_u (.clk(clk), .rst(rst), .go(start));
fsm_plain_always_list plain_always_list_u (.clk(clk), .rst(rst), .go(start));
fsm_caseassigns_off caseassigns_off_u (.clk(clk), .rst(rst), .go(start));
fsm_seqmix_off seqmix_off_u (.clk(clk), .rst(rst));
initial begin
cyc = 0;
rst = 1'b1;
start = 1'b0;
bit_done = 1'b0;
sel = 1'b0;
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 == 4) bit_done <= 1'b1;
if (cyc == 4) sel <= 1'b1;
if (cyc == 5) bit_done <= 1'b0;
if (cyc == 5) sel <= 1'b0;
if (cyc == 8) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
endmodule

View File

@ -12,7 +12,7 @@ from pathlib import Path
import vltest_bootstrap
test.scenarios('vlt')
test.top_filename = "t/t_cover_fsm_styles.v"
test.top_filename = "t/t_cover_fsm_policy_accept_multi.v"
# Dump generic COVEROTHERDECL nodes so AstCoverOtherDecl::dump() also sees
# coverage declarations with no FSM metadata, exercising the empty-field side

View File

@ -0,0 +1,15 @@
%Warning-FSMMULTI: t/t_fsmmulti_combo_multi_warn_bad.v:34:21: FSM coverage: multiple supported transition candidates found in the same combinational always block. Only the first candidate will be instrumented.
34 | B0: state_b_d = B1;
| ^
t/t_fsmmulti_combo_multi_warn_bad.v:30:21: ... Location of first supported candidate for 't.same_u.state_a_q'
30 | A0: state_a_d = A1;
| ^
... 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.
%Warning-FSMMULTI: t/t_fsmmulti_combo_multi_warn_bad.v:70:19: FSM coverage: multiple supported transition candidates found for the same FSM in combinational always blocks. Only the first candidate will be instrumented.
70 | S0: state_d = S1;
| ^
t/t_fsmmulti_combo_multi_warn_bad.v:62:19: ... Location of first supported candidate for 't.split_u.state_q'
62 | S0: state_d = S1;
| ^
%Error: Exiting due to

View File

@ -0,0 +1,16 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: combined FSMMULTI warning regression
#
# 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,86 @@
// DESCRIPTION: Verilator: combined FSMMULTI warning regression
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Group the combo-family multi-candidate warnings where two supported
// transition sites compete and the detector must keep only the first one.
module same_always_warn (
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_q;
a_state_t state_a_d;
b_state_t state_b_q;
b_state_t state_b_d;
always_comb begin
state_a_d = state_a_q;
state_b_d = state_b_q;
case (state_a_q)
A0: state_a_d = A1;
default: state_a_d = A0;
endcase
case (state_b_q)
B0: state_b_d = B1;
default: state_b_d = B0;
endcase
end
always_ff @(posedge clk) begin
state_a_q <= state_a_d;
end
always_ff @(posedge clk) begin
state_b_q <= state_b_d;
end
endmodule
module split_always_warn (
input logic clk
);
/* verilator lint_off MULTIDRIVEN */
typedef enum logic [1:0] {
S0, S1
} state_t;
state_t state_q;
state_t state_d;
always_comb begin
state_d = state_q;
case (state_q)
S0: state_d = S1;
default: state_d = S0;
endcase
end
always_comb begin
state_d = state_q;
case (state_q)
S0: state_d = S1;
default: state_d = S0;
endcase
end
always_ff @(posedge clk) begin
state_q <= state_d;
end
/* verilator lint_on MULTIDRIVEN */
endmodule
module t (
input logic clk
);
same_always_warn same_u (.clk(clk));
split_always_warn split_u (.clk(clk));
endmodule

View File

@ -1,6 +1,9 @@
%Warning-COVERIGN: t/t_fsmmulti_same_bad.v:33:5: Ignoring unsupported: FSM coverage on multiple supported case statements found in the same always block. Only the first candidate will be instrumented.
33 | case (state)
| ^~~~
t/t_fsmmulti_same_bad.v:28:7: ... Location of first supported candidate for 't.state'
28 | 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

@ -1,5 +1,8 @@
%Warning-FSMMULTI: t/t_fsmmulti_warn_bad.v:29:5: FSM coverage: multiple enum-typed case statements found in the same always block. Only the first candidate will be instrumented.
29 | case (state_b)
| ^~~~
t/t_fsmmulti_warn_bad.v:25:5: ... Location of first supported candidate for 't.state_a'
25 | case (state_a)
| ^~~~
... 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.

View File

@ -50,6 +50,8 @@
%000005 case (state_default)
// [FSM coverage]
%000001 // [fsm_arc t.state_default::ANY->S0[reset]] [reset arc, excluded from %]
%000000 // [fsm_arc t.state_default::S0->S1]
%000003 // [fsm_arc t.state_default::S0->S2]
%000000 // [SYNTHETIC DEFAULT ARC: t.state_default::default->S0]
%000002 // [fsm_state t.state_default::S0]
%000000 // [fsm_state t.state_default::S1] *** UNCOVERED ***

View File

@ -50,6 +50,8 @@
%000005 case (state_default)
// [FSM coverage]
%000001 // [fsm_arc t.state_default::ANY->S0[reset]]
%000000 // [fsm_arc t.state_default::S0->S1]
%000003 // [fsm_arc t.state_default::S0->S2]
%000000 // [SYNTHETIC DEFAULT ARC: t.state_default::default->S0]
%000002 // [fsm_state t.state_default::S0]
%000000 // [fsm_state t.state_default::S1] *** UNCOVERED ***