diff --git a/src/V3FsmDetect.cpp b/src/V3FsmDetect.cpp index 3a9d48877..f09f91163 100644 --- a/src/V3FsmDetect.cpp +++ b/src/V3FsmDetect.cpp @@ -34,6 +34,7 @@ #include #include #include +#include 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 m_senses; // Event controls for recreated coverage blocks. + FsmResetCondDesc m_resetCond; // Saved reset predicate, if any. + std::vector 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& senses() const { return m_senses; } + std::vector& senses() { return m_senses; } + const FsmResetCondDesc& resetCond() const { return m_resetCond; } + FsmResetCondDesc& resetCond() { return m_resetCond; } + const std::vector& resetArcs() const { return m_resetArcs; } + std::vector& 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 graphp; // Extracted graph for one detected FSM candidate. }; -using DetectedFsmMap = std::map; +using DetectedFsmMap = std::map; + +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 m_registerCandidates; + std::vector m_comboAlwayss; + std::vector m_nonComboAlwayss; + std::unordered_map 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> oneBlockCandidates(AstAlways* alwaysp) const { + std::vector> 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& m_registerCandidates; + + public: + explicit ComboAlwaysAnalyzer( + const std::unordered_map& 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 = ® + 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& 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& labels, int value) { @@ -356,10 +778,105 @@ class FsmDetectVisitor final : public VNVisitor { const std::unordered_map& 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>& states, + std::unordered_map& 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(value)); + labels.emplace(static_cast(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& 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& 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& resetArcs, const std::unordered_map& 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> states; std::unordered_map 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(value)); - labels.emplace(static_cast(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& 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> 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> candidates + = analyzer.oneBlockCandidates(alwaysp); if (candidates.empty()) return; - AstVarScope* firstVscp = nullptr; + FsmCaseCandidate firstCand; for (const std::pair& 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 labels; - for (const V3GraphVertex& vtx : graphp->vertices()) { - const FsmVertex* const vertexp = vtx.as(); - 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( + 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(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& 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& it : state.fsms()) { + for (const auto& it : state.fsms()) { it.second.graphp->dumpDotFilePrefixed(it.second.graphp->dumpTag(index++)); } } diff --git a/test_regress/t/t_cover_fsm_basic.py b/test_regress/t/t_cover_fsm_basic.py index 728bcebef..628914b4f 100755 --- a/test_regress/t/t_cover_fsm_basic.py +++ b/test_regress/t/t_cover_fsm_basic.py @@ -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", diff --git a/test_regress/t/t_cover_fsm_basic_summary.out b/test_regress/t/t_cover_fsm_basic_summary.out deleted file mode 100644 index a0e1606ad..000000000 --- a/test_regress/t/t_cover_fsm_basic_summary.out +++ /dev/null @@ -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) diff --git a/test_regress/t/t_cover_fsm_beginif.out b/test_regress/t/t_cover_fsm_beginif.out index b7aca2059..314d1b5aa 100644 --- a/test_regress/t/t_cover_fsm_beginif.out +++ b/test_regress/t/t_cover_fsm_beginif.out @@ -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 diff --git a/test_regress/t/t_cover_fsm_beginif.v b/test_regress/t/t_cover_fsm_beginif.v index 4ad6f1437..b42d792cc 100644 --- a/test_regress/t/t_cover_fsm_beginif.v +++ b/test_regress/t/t_cover_fsm_beginif.v @@ -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 diff --git a/test_regress/t/t_cover_fsm_case_next_ok_multi.out b/test_regress/t/t_cover_fsm_case_next_ok_multi.out new file mode 100644 index 000000000..776f62afa --- /dev/null +++ b/test_regress/t/t_cover_fsm_case_next_ok_multi.out @@ -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 + diff --git a/test_regress/t/t_cover_fsm_case_next_ok_multi.py b/test_regress/t/t_cover_fsm_case_next_ok_multi.py new file mode 100755 index 000000000..a95c8efad --- /dev/null +++ b/test_regress/t/t_cover_fsm_case_next_ok_multi.py @@ -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() diff --git a/test_regress/t/t_cover_fsm_case_next_ok_multi.v b/test_regress/t/t_cover_fsm_case_next_ok_multi.v new file mode 100644 index 000000000..ac818c228 --- /dev/null +++ b/test_regress/t/t_cover_fsm_case_next_ok_multi.v @@ -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 diff --git a/test_regress/t/t_cover_fsm_case_next_ok_multi_incl.out b/test_regress/t/t_cover_fsm_case_next_ok_multi_incl.out new file mode 100644 index 000000000..1b3f91613 --- /dev/null +++ b/test_regress/t/t_cover_fsm_case_next_ok_multi_incl.out @@ -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 + diff --git a/test_regress/t/t_cover_fsm_combo_same_warn_bad.out b/test_regress/t/t_cover_fsm_combo_same_warn_bad.out new file mode 100644 index 000000000..32f725885 --- /dev/null +++ b/test_regress/t/t_cover_fsm_combo_same_warn_bad.out @@ -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 diff --git a/test_regress/t/t_cover_fsm_combo_same_warn_bad.py b/test_regress/t/t_cover_fsm_combo_same_warn_bad.py new file mode 100755 index 000000000..46cc6db99 --- /dev/null +++ b/test_regress/t/t_cover_fsm_combo_same_warn_bad.py @@ -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() diff --git a/test_regress/t/t_cover_fsm_combo_same_warn_bad.v b/test_regress/t/t_cover_fsm_combo_same_warn_bad.v new file mode 100644 index 000000000..fa6966182 --- /dev/null +++ b/test_regress/t/t_cover_fsm_combo_same_warn_bad.v @@ -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 diff --git a/test_regress/t/t_cover_fsm_decldump.py b/test_regress/t/t_cover_fsm_decldump.py index dc2cd087d..66c75caa2 100755 --- a/test_regress/t/t_cover_fsm_decldump.py +++ b/test_regress/t/t_cover_fsm_decldump.py @@ -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) diff --git a/test_regress/t/t_cover_fsm_enum_bad.out b/test_regress/t/t_cover_fsm_enum_bad.out deleted file mode 100644 index 72de142b4..000000000 --- a/test_regress/t/t_cover_fsm_enum_bad.out +++ /dev/null @@ -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 diff --git a/test_regress/t/t_cover_fsm_enum_bad.v b/test_regress/t/t_cover_fsm_enum_bad.v deleted file mode 100644 index cd7ac7442..000000000 --- a/test_regress/t/t_cover_fsm_enum_bad.v +++ /dev/null @@ -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 diff --git a/test_regress/t/t_cover_fsm_forced.out b/test_regress/t/t_cover_fsm_forced.out deleted file mode 100644 index e0112cee6..000000000 --- a/test_regress/t/t_cover_fsm_forced.out +++ /dev/null @@ -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 - diff --git a/test_regress/t/t_cover_fsm_forced.v b/test_regress/t/t_cover_fsm_forced.v deleted file mode 100644 index 974aee868..000000000 --- a/test_regress/t/t_cover_fsm_forced.v +++ /dev/null @@ -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 diff --git a/test_regress/t/t_cover_fsm_graphdump.py b/test_regress/t/t_cover_fsm_graphdump.py index 3290be13f..9742f2aaf 100755 --- a/test_regress/t/t_cover_fsm_graphdump.py +++ b/test_regress/t/t_cover_fsm_graphdump.py @@ -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) diff --git a/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.out b/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.out new file mode 100644 index 000000000..ba8106741 --- /dev/null +++ b/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.out @@ -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 diff --git a/test_regress/t/t_cover_fsm_enum_bad.py b/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.py similarity index 63% rename from test_regress/t/t_cover_fsm_enum_bad.py rename to test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.py index 541252ec8..936ca5648 100755 --- a/test_regress/t/t_cover_fsm_enum_bad.py +++ b/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.py @@ -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() diff --git a/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.v b/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.v new file mode 100644 index 000000000..981f9f5e1 --- /dev/null +++ b/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.v @@ -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 diff --git a/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.out b/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.out new file mode 100644 index 000000000..71f746138 --- /dev/null +++ b/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.out @@ -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 diff --git a/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.py b/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.py new file mode 100755 index 000000000..40fb48177 --- /dev/null +++ b/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.py @@ -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() diff --git a/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.v b/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.v new file mode 100644 index 000000000..1e13f5570 --- /dev/null +++ b/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.v @@ -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 diff --git a/test_regress/t/t_cover_fsm_plain_always_ignore_multi.out b/test_regress/t/t_cover_fsm_plain_always_ignore_multi.out new file mode 100644 index 000000000..8b8f255e5 --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_ignore_multi.out @@ -0,0 +1 @@ +# SystemC::Coverage-3 diff --git a/test_regress/t/t_cover_fsm_plain_always_ignore_multi.py b/test_regress/t/t_cover_fsm_plain_always_ignore_multi.py new file mode 100755 index 000000000..1b2d7e17a --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_ignore_multi.py @@ -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() diff --git a/test_regress/t/t_cover_fsm_plain_always_ignore_multi.v b/test_regress/t/t_cover_fsm_plain_always_ignore_multi.v new file mode 100644 index 000000000..ed04b539d --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_ignore_multi.v @@ -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 diff --git a/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.out b/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.out new file mode 100644 index 000000000..064ef2ff1 --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.out @@ -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 diff --git a/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.py b/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.py new file mode 100755 index 000000000..57716476b --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.py @@ -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() diff --git a/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.v b/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.v new file mode 100644 index 000000000..8194799f6 --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.v @@ -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 diff --git a/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.out b/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.out new file mode 100644 index 000000000..05101a413 --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.out @@ -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 diff --git a/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.py b/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.py new file mode 100644 index 000000000..163436db8 --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.py @@ -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() diff --git a/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.v b/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.v new file mode 100644 index 000000000..d8b63f3fb --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.v @@ -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 diff --git a/test_regress/t/t_cover_fsm_policy_accept_multi.out b/test_regress/t/t_cover_fsm_policy_accept_multi.out new file mode 100644 index 000000000..c2c35da00 --- /dev/null +++ b/test_regress/t/t_cover_fsm_policy_accept_multi.out @@ -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 + diff --git a/test_regress/t/t_cover_fsm_forced.py b/test_regress/t/t_cover_fsm_policy_accept_multi.py similarity index 78% rename from test_regress/t/t_cover_fsm_forced.py rename to test_regress/t/t_cover_fsm_policy_accept_multi.py index f368d9cf9..1b075a66f 100755 --- a/test_regress/t/t_cover_fsm_forced.py +++ b/test_regress/t/t_cover_fsm_policy_accept_multi.py @@ -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", diff --git a/test_regress/t/t_cover_fsm_policy_accept_multi.v b/test_regress/t/t_cover_fsm_policy_accept_multi.v new file mode 100644 index 000000000..ecd6ac21a --- /dev/null +++ b/test_regress/t/t_cover_fsm_policy_accept_multi.v @@ -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 diff --git a/test_regress/t/t_cover_fsm_reset.out b/test_regress/t/t_cover_fsm_reset.out index ddbd755f8..8e7ec3c61 100644 --- a/test_regress/t/t_cover_fsm_reset.out +++ b/test_regress/t/t_cover_fsm_reset.out @@ -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 diff --git a/test_regress/t/t_cover_fsm_reset.py b/test_regress/t/t_cover_fsm_reset.py index ee7db650a..13a780eaa 100755 --- a/test_regress/t/t_cover_fsm_reset.py +++ b/test_regress/t/t_cover_fsm_reset.py @@ -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() diff --git a/test_regress/t/t_cover_fsm_reset.v b/test_regress/t/t_cover_fsm_reset.v index 4303fa771..5deaeb08a 100644 --- a/test_regress/t/t_cover_fsm_reset.v +++ b/test_regress/t/t_cover_fsm_reset.v @@ -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 diff --git a/test_regress/t/t_cover_fsm_reset_incl.out b/test_regress/t/t_cover_fsm_reset_incl.out new file mode 100644 index 000000000..7f79dfb75 --- /dev/null +++ b/test_regress/t/t_cover_fsm_reset_incl.out @@ -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 + diff --git a/test_regress/t/t_cover_fsm_reset_multi.out b/test_regress/t/t_cover_fsm_reset_multi.out index c74be0d53..dd19f5207 100644 --- a/test_regress/t/t_cover_fsm_reset_multi.out +++ b/test_regress/t/t_cover_fsm_reset_multi.out @@ -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 diff --git a/test_regress/t/t_cover_fsm_reset_multi.py b/test_regress/t/t_cover_fsm_reset_multi.py index 42ab4d0f3..6689f3ab2 100755 --- a/test_regress/t/t_cover_fsm_reset_multi.py +++ b/test_regress/t/t_cover_fsm_reset_multi.py @@ -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() diff --git a/test_regress/t/t_cover_fsm_reset_multi.v b/test_regress/t/t_cover_fsm_reset_multi.v index 31dc4e748..bf7e87bf7 100644 --- a/test_regress/t/t_cover_fsm_reset_multi.v +++ b/test_regress/t/t_cover_fsm_reset_multi.v @@ -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; diff --git a/test_regress/t/t_cover_fsm_reset_summary.out b/test_regress/t/t_cover_fsm_reset_summary.out deleted file mode 100644 index 32439a8ea..000000000 --- a/test_regress/t/t_cover_fsm_reset_summary.out +++ /dev/null @@ -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) diff --git a/test_regress/t/t_cover_fsm_styles.out b/test_regress/t/t_cover_fsm_styles.out deleted file mode 100644 index ac3171825..000000000 --- a/test_regress/t/t_cover_fsm_styles.out +++ /dev/null @@ -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 - diff --git a/test_regress/t/t_cover_fsm_styles.v b/test_regress/t/t_cover_fsm_styles.v deleted file mode 100644 index a07022b10..000000000 --- a/test_regress/t/t_cover_fsm_styles.v +++ /dev/null @@ -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 diff --git a/test_regress/t/t_cover_fsm_transition_shapes_multi.out b/test_regress/t/t_cover_fsm_transition_shapes_multi.out new file mode 100644 index 000000000..8b8f255e5 --- /dev/null +++ b/test_regress/t/t_cover_fsm_transition_shapes_multi.out @@ -0,0 +1 @@ +# SystemC::Coverage-3 diff --git a/test_regress/t/t_cover_fsm_styles.py b/test_regress/t/t_cover_fsm_transition_shapes_multi.py similarity index 54% rename from test_regress/t/t_cover_fsm_styles.py rename to test_regress/t/t_cover_fsm_transition_shapes_multi.py index fc9fde9f0..46c209f35 100755 --- a/test_regress/t/t_cover_fsm_styles.py +++ b/test_regress/t/t_cover_fsm_transition_shapes_multi.py @@ -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() diff --git a/test_regress/t/t_cover_fsm_transition_shapes_multi.v b/test_regress/t/t_cover_fsm_transition_shapes_multi.v new file mode 100644 index 000000000..ae7103724 --- /dev/null +++ b/test_regress/t/t_cover_fsm_transition_shapes_multi.v @@ -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 diff --git a/test_regress/t/t_cover_fsm_two_proc_multi.out b/test_regress/t/t_cover_fsm_two_proc_multi.out new file mode 100644 index 000000000..42e8edefd --- /dev/null +++ b/test_regress/t/t_cover_fsm_two_proc_multi.out @@ -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 + diff --git a/test_regress/t/t_cover_fsm_two_proc_multi.py b/test_regress/t/t_cover_fsm_two_proc_multi.py new file mode 100755 index 000000000..0768245c3 --- /dev/null +++ b/test_regress/t/t_cover_fsm_two_proc_multi.py @@ -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() diff --git a/test_regress/t/t_cover_fsm_two_proc_multi.v b/test_regress/t/t_cover_fsm_two_proc_multi.v new file mode 100644 index 000000000..6cb3c7354 --- /dev/null +++ b/test_regress/t/t_cover_fsm_two_proc_multi.v @@ -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 diff --git a/test_regress/t/t_cover_otherdecl_dump.py b/test_regress/t/t_cover_otherdecl_dump.py index a1524c584..a82316eea 100755 --- a/test_regress/t/t_cover_otherdecl_dump.py +++ b/test_regress/t/t_cover_otherdecl_dump.py @@ -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 diff --git a/test_regress/t/t_fsmmulti_combo_multi_warn_bad.out b/test_regress/t/t_fsmmulti_combo_multi_warn_bad.out new file mode 100644 index 000000000..9d16b62e2 --- /dev/null +++ b/test_regress/t/t_fsmmulti_combo_multi_warn_bad.out @@ -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 diff --git a/test_regress/t/t_fsmmulti_combo_multi_warn_bad.py b/test_regress/t/t_fsmmulti_combo_multi_warn_bad.py new file mode 100755 index 000000000..9359d4535 --- /dev/null +++ b/test_regress/t/t_fsmmulti_combo_multi_warn_bad.py @@ -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() diff --git a/test_regress/t/t_fsmmulti_combo_multi_warn_bad.v b/test_regress/t/t_fsmmulti_combo_multi_warn_bad.v new file mode 100644 index 000000000..f21c56bf9 --- /dev/null +++ b/test_regress/t/t_fsmmulti_combo_multi_warn_bad.v @@ -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 diff --git a/test_regress/t/t_fsmmulti_same_bad.out b/test_regress/t/t_fsmmulti_same_bad.out index d48efde98..1efc9fc77 100644 --- a/test_regress/t/t_fsmmulti_same_bad.out +++ b/test_regress/t/t_fsmmulti_same_bad.out @@ -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 diff --git a/test_regress/t/t_fsmmulti_warn_bad.out b/test_regress/t/t_fsmmulti_warn_bad.out index 27e284264..82d64e44b 100644 --- a/test_regress/t/t_fsmmulti_warn_bad.out +++ b/test_regress/t/t_fsmmulti_warn_bad.out @@ -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. diff --git a/test_regress/t/t_vlcov_fsm_report.out b/test_regress/t/t_vlcov_fsm_report.out index 1c5e71b72..d6598b075 100644 --- a/test_regress/t/t_vlcov_fsm_report.out +++ b/test_regress/t/t_vlcov_fsm_report.out @@ -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 *** diff --git a/test_regress/t/t_vlcov_fsm_report_incl.out b/test_regress/t/t_vlcov_fsm_report_incl.out index ccf63b0b1..31117beb4 100644 --- a/test_regress/t/t_vlcov_fsm_report_incl.out +++ b/test_regress/t/t_vlcov_fsm_report_incl.out @@ -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 ***