From a680919edc15822d2888e4a1995a79fdc6094c65 Mon Sep 17 00:00:00 2001 From: Yogish Sekhar <160029258+ysekhar@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:18:59 +0100 Subject: [PATCH] Support native FSM state and arc coverage (#7412) --- bin/verilator | 1 + bin/verilator_coverage | 1 + docs/guide/exe_verilator.rst | 7 +- docs/guide/exe_verilator_coverage.rst | 11 +- docs/guide/simulating.rst | 42 + docs/guide/warnings.rst | 14 +- docs/spelling.txt | 4 + include/verilated_cov.cpp | 20 + include/verilated_cov.h | 7 + include/verilated_cov_key.h | 12 + src/CMakeLists.txt | 2 + src/Makefile_obj.in | 1 + src/V3AstAttr.h | 11 +- src/V3AstNodeOther.h | 28 +- src/V3AstNodes.cpp | 14 + src/V3EmitCFunc.h | 8 + src/V3EmitCHeaders.cpp | 4 +- src/V3EmitCImp.cpp | 14 +- src/V3Error.h | 19 +- src/V3FsmDetect.cpp | 785 ++++++++++++++++++ src/V3FsmDetect.h | 33 + src/V3LinkParse.cpp | 12 + src/V3Options.cpp | 1 + src/V3Options.h | 14 +- src/Verilator.cpp | 22 +- src/VlcMain.cpp | 1 + src/VlcOptions.h | 2 + src/VlcPoint.h | 12 + src/VlcTop.cpp | 41 +- src/verilog.l | 8 + src/verilog.y | 6 + test_regress/t/t_cover_fsm_basic.out | 65 ++ test_regress/t/t_cover_fsm_basic.py | 30 + test_regress/t/t_cover_fsm_basic.v | 53 ++ test_regress/t/t_cover_fsm_beginif.out | 63 ++ test_regress/t/t_cover_fsm_beginif.py | 31 + test_regress/t/t_cover_fsm_beginif.v | 53 ++ test_regress/t/t_cover_fsm_decldump.py | 36 + test_regress/t/t_cover_fsm_enum_bad.out | 6 + test_regress/t/t_cover_fsm_enum_bad.py | 26 + test_regress/t/t_cover_fsm_enum_bad.v | 34 + test_regress/t/t_cover_fsm_enumwide_bad.out | 6 + test_regress/t/t_cover_fsm_enumwide_bad.py | 25 + test_regress/t/t_cover_fsm_enumwide_bad.v | 32 + test_regress/t/t_cover_fsm_flag_off.py | 21 + test_regress/t/t_cover_fsm_flag_off.v | 53 ++ test_regress/t/t_cover_fsm_forced.out | 51 ++ test_regress/t/t_cover_fsm_forced.py | 32 + test_regress/t/t_cover_fsm_forced.v | 41 + test_regress/t/t_cover_fsm_graphdump.py | 24 + .../t/t_cover_fsm_negative_extract.out | 64 ++ .../t/t_cover_fsm_negative_extract.py | 37 + test_regress/t/t_cover_fsm_negative_extract.v | 57 ++ test_regress/t/t_cover_fsm_noreset.out | 47 ++ test_regress/t/t_cover_fsm_noreset.py | 36 + test_regress/t/t_cover_fsm_noreset.v | 41 + test_regress/t/t_cover_fsm_reset.out | 63 ++ test_regress/t/t_cover_fsm_reset.py | 37 + test_regress/t/t_cover_fsm_reset.v | 51 ++ test_regress/t/t_cover_fsm_reset_multi.out | 62 ++ test_regress/t/t_cover_fsm_reset_multi.py | 36 + test_regress/t/t_cover_fsm_reset_multi.v | 52 ++ test_regress/t/t_cover_fsm_styles.out | 64 ++ test_regress/t/t_cover_fsm_styles.py | 27 + test_regress/t/t_cover_fsm_styles.v | 52 ++ test_regress/t/t_cover_otherdecl_dump.py | 35 + test_regress/t/t_fsm_metacmt_dump.py | 35 + test_regress/t/t_fsm_metacmt_dump.v | 34 + test_regress/t/t_fsmmulti_same_bad.out | 6 + test_regress/t/t_fsmmulti_same_bad.py | 28 + test_regress/t/t_fsmmulti_same_bad.v | 36 + test_regress/t/t_fsmmulti_warn_bad.out | 6 + test_regress/t/t_fsmmulti_warn_bad.py | 19 + test_regress/t/t_fsmmulti_warn_bad.v | 33 + test_regress/t/t_fsmmulti_warn_off.py | 17 + test_regress/t/t_fsmmulti_warn_off.v | 42 + test_regress/t/t_vlcov_fsm_report.out | 102 +++ test_regress/t/t_vlcov_fsm_report.py | 53 ++ test_regress/t/t_vlcov_fsm_report.v | 79 ++ test_regress/t/t_vlcov_fsm_report_incl.out | 102 +++ 80 files changed, 3157 insertions(+), 35 deletions(-) create mode 100644 src/V3FsmDetect.cpp create mode 100644 src/V3FsmDetect.h create mode 100644 test_regress/t/t_cover_fsm_basic.out create mode 100644 test_regress/t/t_cover_fsm_basic.py create mode 100644 test_regress/t/t_cover_fsm_basic.v create mode 100644 test_regress/t/t_cover_fsm_beginif.out create mode 100644 test_regress/t/t_cover_fsm_beginif.py create mode 100644 test_regress/t/t_cover_fsm_beginif.v create mode 100644 test_regress/t/t_cover_fsm_decldump.py create mode 100644 test_regress/t/t_cover_fsm_enum_bad.out create mode 100644 test_regress/t/t_cover_fsm_enum_bad.py create mode 100644 test_regress/t/t_cover_fsm_enum_bad.v create mode 100644 test_regress/t/t_cover_fsm_enumwide_bad.out create mode 100644 test_regress/t/t_cover_fsm_enumwide_bad.py create mode 100644 test_regress/t/t_cover_fsm_enumwide_bad.v create mode 100644 test_regress/t/t_cover_fsm_flag_off.py create mode 100644 test_regress/t/t_cover_fsm_flag_off.v create mode 100644 test_regress/t/t_cover_fsm_forced.out create mode 100644 test_regress/t/t_cover_fsm_forced.py create mode 100644 test_regress/t/t_cover_fsm_forced.v create mode 100644 test_regress/t/t_cover_fsm_graphdump.py create mode 100644 test_regress/t/t_cover_fsm_negative_extract.out create mode 100644 test_regress/t/t_cover_fsm_negative_extract.py create mode 100644 test_regress/t/t_cover_fsm_negative_extract.v create mode 100644 test_regress/t/t_cover_fsm_noreset.out create mode 100644 test_regress/t/t_cover_fsm_noreset.py create mode 100644 test_regress/t/t_cover_fsm_noreset.v create mode 100644 test_regress/t/t_cover_fsm_reset.out create mode 100644 test_regress/t/t_cover_fsm_reset.py create mode 100644 test_regress/t/t_cover_fsm_reset.v create mode 100644 test_regress/t/t_cover_fsm_reset_multi.out create mode 100644 test_regress/t/t_cover_fsm_reset_multi.py create mode 100644 test_regress/t/t_cover_fsm_reset_multi.v create mode 100644 test_regress/t/t_cover_fsm_styles.out create mode 100644 test_regress/t/t_cover_fsm_styles.py create mode 100644 test_regress/t/t_cover_fsm_styles.v create mode 100644 test_regress/t/t_cover_otherdecl_dump.py create mode 100644 test_regress/t/t_fsm_metacmt_dump.py create mode 100644 test_regress/t/t_fsm_metacmt_dump.v create mode 100644 test_regress/t/t_fsmmulti_same_bad.out create mode 100644 test_regress/t/t_fsmmulti_same_bad.py create mode 100644 test_regress/t/t_fsmmulti_same_bad.v create mode 100644 test_regress/t/t_fsmmulti_warn_bad.out create mode 100644 test_regress/t/t_fsmmulti_warn_bad.py create mode 100644 test_regress/t/t_fsmmulti_warn_bad.v create mode 100644 test_regress/t/t_fsmmulti_warn_off.py create mode 100644 test_regress/t/t_fsmmulti_warn_off.v create mode 100644 test_regress/t/t_vlcov_fsm_report.out create mode 100644 test_regress/t/t_vlcov_fsm_report.py create mode 100644 test_regress/t/t_vlcov_fsm_report.v create mode 100644 test_regress/t/t_vlcov_fsm_report_incl.out diff --git a/bin/verilator b/bin/verilator index 5f5fd426f..78d1088f5 100755 --- a/bin/verilator +++ b/bin/verilator @@ -358,6 +358,7 @@ detailed descriptions of these arguments. --coverage Enable all coverage --coverage-expr Enable expression coverage --coverage-expr-max Maximum permutations allowed for an expression + --coverage-fsm Enable FSM state/arc coverage --coverage-line Enable line coverage --coverage-max-width Maximum array depth for coverage --coverage-toggle Enable toggle coverage diff --git a/bin/verilator_coverage b/bin/verilator_coverage index 9f0b22e16..be48ff433 100755 --- a/bin/verilator_coverage +++ b/bin/verilator_coverage @@ -175,6 +175,7 @@ L. --annotate-points Annotates info from each coverage point. --filter-type Keep only records of given coverage type. --help Displays this message and version and exits. + --include-reset-arcs Include reset arcs in FSM arc summaries. --rank Compute relative importance of tests. --unlink With --write, unlink all inputs --version Displays program version and exits. diff --git a/docs/guide/exe_verilator.rst b/docs/guide/exe_verilator.rst index eaf670669..23cdead2f 100644 --- a/docs/guide/exe_verilator.rst +++ b/docs/guide/exe_verilator.rst @@ -281,7 +281,8 @@ Summary: .. option:: --coverage Enables all forms of coverage, an alias for :vlopt:`--coverage-line` - :vlopt:`--coverage-toggle` :vlopt:`--coverage-expr` :vlopt:`--coverage-user`. + :vlopt:`--coverage-toggle` :vlopt:`--coverage-expr` :vlopt:`--coverage-fsm` + :vlopt:`--coverage-user`. .. option:: --coverage-expr @@ -293,6 +294,10 @@ Summary: covered for a given expression. Defaults to 32. Increasing may slow coverage simulations and make analyzing the results unwieldy. +.. option:: --coverage-fsm + + Enables native FSM state and arc coverage. See :ref:`FSM Coverage`. + .. option:: --coverage-line Enables basic block line coverage analysis. See :ref:`Line Coverage`. diff --git a/docs/guide/exe_verilator_coverage.rst b/docs/guide/exe_verilator_coverage.rst index 23bbd6b8f..2eac8e5fc 100644 --- a/docs/guide/exe_verilator_coverage.rst +++ b/docs/guide/exe_verilator_coverage.rst @@ -129,13 +129,20 @@ verilator_coverage Arguments .. option:: --filter-type Skips records of coverage types that matches with - Possible values are `toggle`, `line`, `branch`, `expr`, `user` and - a wildcard with `\*` or `?`. The default value is `\*`. + Possible values are `toggle`, `line`, `branch`, `expr`, `user`, + `fsm_state`, `fsm_arc` and a wildcard with `\*` or `?`. The default + value is `\*`. .. option:: --help Displays a help summary, the program version, and exits. +.. option:: --include-reset-arcs + + Includes FSM reset arcs in the printed summaries and annotated output. + By default, reset arcs are tracked but summarized separately from the + non-reset FSM arcs. + .. option:: --rank Prints an experimental report listing the relative importance of each diff --git a/docs/guide/simulating.rst b/docs/guide/simulating.rst index fd92bb1ba..5a4878dd9 100644 --- a/docs/guide/simulating.rst +++ b/docs/guide/simulating.rst @@ -185,6 +185,7 @@ SystemVerilog code coverage. With :vlopt:`--coverage`, Verilator enables all forms of coverage: - :ref:`User Coverage` +- :ref:`FSM Coverage` - :ref:`Line Coverage` - :ref:`Toggle Coverage` @@ -208,6 +209,47 @@ point under the coverage name "DefaultClock": DefaultClock: cover property (@(posedge clk) cyc==3); +.. _fsm coverage: + +FSM Coverage +------------ + +With :vlopt:`--coverage` or :vlopt:`--coverage-fsm`, Verilator can +instrument a conservative subset of single-process FSMs and report both +state coverage (`fsm_state`) and transition coverage (`fsm_arc`). + +This feature is currently experimental and might change in subsequent +releases. In particular, the native FSM coverage extraction heuristics, +:vlopt:`--coverage-fsm`, and the Verilator-specific FSM metacomments below +should be treated as subject to change while the interface settles. + +FSM extraction is intentionally narrow. The current implementation targets +clocked, enum-driven state machines that can be recovered directly from the +RTL. It does not claim broad support for two-process FSMs, one-hot +inference, helper-function next-state recovery, or deeply nested control +recovery. + +The following metacomments may be attached to the state variable to steer +the extracted coverage model: + +- ``/*verilator fsm_state*/`` forces the variable to be treated as + FSM state. +- ``/*verilator fsm_reset_arc*/`` marks reset transitions as + user-visible reset arcs instead of defaulting to a hidden reset-only + summary. +- ``/*verilator fsm_arc_include_cond*/`` keeps conditional branch + arcs that would otherwise be skipped by the conservative extractor. + +Reset transitions are included in the collected data either way. By +default, :command:`verilator_coverage` summarizes reset-only arcs rather +than printing them alongside non-reset arcs. Use +:option:`verilator_coverage --include-reset-arcs` to include +those arcs in the printed summary and annotated output. + +Annotated output produced by :command:`verilator_coverage --annotate` will +label FSM points with `fsm_state` and `fsm_arc`, and synthetic fallback +transitions with `SYNTHETIC DEFAULT ARC`. + .. _line coverage: diff --git a/docs/guide/warnings.rst b/docs/guide/warnings.rst index 53c2958ab..0ba178158 100644 --- a/docs/guide/warnings.rst +++ b/docs/guide/warnings.rst @@ -306,7 +306,7 @@ List Of Warnings else array[address] <= data; - While this is supported in typical synthesizeable code (including the + While this is supported in typical synthesizable code (including the example above), some complicated cases are not supported. Namely: 1. If the above loop is inside a suspendable process or fork statement. @@ -837,6 +837,18 @@ List Of Warnings with a newline." +.. option:: FSMMULTI + + Warns that the same always block contains multiple enum-typed case + statements that look like FSM candidates for native FSM coverage when + :vlopt:`--coverage-fsm` or :vlopt:`--coverage` is enabled. + + Verilator's FSM coverage instruments only the first such candidate in + source order. Split the FSMs into separate always blocks, or explicitly + annotate the intended state variables and restructure the RTL for full + coverage of such multiple state machines. + + .. option:: FUNCTIMECTL Error that a function contains a time-controlling statement or call of a diff --git a/docs/spelling.txt b/docs/spelling.txt index 36db92d3a..b25c946e1 100644 --- a/docs/spelling.txt +++ b/docs/spelling.txt @@ -2,6 +2,7 @@ ABCp Aadi Accellera Aditya +allocator Affe Aleksander Alexandre @@ -362,6 +363,7 @@ Olofsson Ondrej Oron Oyvind +output PLI Pakanati Palaniappan @@ -402,8 +404,10 @@ Ranjan Rapp Redhat Reitan +reentrant Renga Requin +reusability Riaz Rodas Rodionov diff --git a/include/verilated_cov.cpp b/include/verilated_cov.cpp index 0f8eced98..1370c38ec 100644 --- a/include/verilated_cov.cpp +++ b/include/verilated_cov.cpp @@ -499,6 +499,26 @@ void VerilatedCovContext::_insertp(A(0), A(1), A(2), A(3), A(4), A(5), A(6), A(7 C(13), C(14), C(15), C(16), C(17), C(18), C(19), N(20), N(21), N(22), N(23), N(24), N(25), N(26), N(27), N(28), N(29)); } +// Backward compatibility for mixed inserts with integer-valued +// lineno/column pairs and C-string-valued metadata pairs. +void VerilatedCovContext::_insertp(A(0), A(1), K(2), int val2, K(3), int val3, A(4), A(5), A(6), + A(7)) VL_MT_SAFE { + const std::string val2str = std::to_string(val2); + const std::string val3str = std::to_string(val3); + _insertp(C(0), C(1), key2, val2str.c_str(), key3, val3str.c_str(), C(4), C(5), C(6), C(7), + N(8), N(9), N(10), N(11), N(12), N(13), N(14), N(15), N(16), N(17), N(18), N(19), + N(20), N(21), N(22), N(23), N(24), N(25), N(26), N(27), N(28), N(29)); +} +// Backward compatibility for mixed inserts with integer-valued +// lineno/column pairs and additional FSM metadata pairs. +void VerilatedCovContext::_insertp(A(0), A(1), K(2), int val2, K(3), int val3, A(4), A(5), A(6), + A(7), A(8), A(9), A(10), A(11)) VL_MT_SAFE { + const std::string val2str = std::to_string(val2); + const std::string val3str = std::to_string(val3); + _insertp(C(0), C(1), key2, val2str.c_str(), key3, val3str.c_str(), C(4), C(5), C(6), C(7), + C(8), C(9), C(10), C(11), N(12), N(13), N(14), N(15), N(16), N(17), N(18), N(19), + N(20), N(21), N(22), N(23), N(24), N(25), N(26), N(27), N(28), N(29)); +} // Backward compatibility for Verilator void VerilatedCovContext::_insertp(A(0), A(1), K(2), int val2, K(3), int val3, K(4), const std::string& val4, A(5), A(6), A(7)) VL_MT_SAFE { diff --git a/include/verilated_cov.h b/include/verilated_cov.h index a6cbd2d4e..e1dcad8d2 100644 --- a/include/verilated_cov.h +++ b/include/verilated_cov.h @@ -191,6 +191,13 @@ public: void _insertp(A(0), A(1), A(2), A(3), A(4), A(5), A(6), A(7), A(8), A(9), A(10), A(11), A(12), A(13), A(14), A(15), A(16), A(17), A(18), A(19), A(20), D(21), D(22), D(23), D(24), D(25), D(26), D(27), D(28), D(29)) VL_MT_SAFE; + // Backward compatibility for mixed inserts with integer-valued + // lineno/column pairs and C-string-valued metadata pairs. + void _insertp(A(0), A(1), K(2), int val2, K(3), int val3, A(4), A(5), A(6), A(7)) VL_MT_SAFE; + // Backward compatibility for mixed inserts with integer-valued + // lineno/column pairs and additional FSM metadata pairs. + void _insertp(A(0), A(1), K(2), int val2, K(3), int val3, A(4), A(5), A(6), A(7), A(8), A(9), + A(10), A(11)) VL_MT_SAFE; // Backward compatibility for Verilator void _insertp(A(0), A(1), K(2), int val2, K(3), int val3, K(4), const std::string& val4, A(5), A(6), A(7)) VL_MT_SAFE; diff --git a/include/verilated_cov_key.h b/include/verilated_cov_key.h index cb0a2efa9..fd7947aa2 100644 --- a/include/verilated_cov_key.h +++ b/include/verilated_cov_key.h @@ -40,6 +40,10 @@ VLCOVGEN_ITEM("'name':'thresh', 'short':'s', 'group':1, 'default':None, 'd VLCOVGEN_ITEM("'name':'type', 'short':'t', 'group':1, 'default':'', 'descr':'Type of coverage (block, line, fsm, etc)'") // Bin attributes VLCOVGEN_ITEM("'name':'comment', 'short':'o', 'group':0, 'default':'', 'descr':'Textual description for the item'") +VLCOVGEN_ITEM("'name':'fsm_from', 'short':'Ff', 'group':0, 'default':'', 'descr':'FSM source state name for structured FSM coverage points'") +VLCOVGEN_ITEM("'name':'fsm_tag', 'short':'Fg', 'group':0, 'default':'', 'descr':'FSM point tag such as reset, reset_include, or default'") +VLCOVGEN_ITEM("'name':'fsm_to', 'short':'Ft', 'group':0, 'default':'', 'descr':'FSM destination state name for structured FSM coverage points'") +VLCOVGEN_ITEM("'name':'fsm_var', 'short':'Fv', 'group':0, 'default':'', 'descr':'FSM state variable name for structured FSM coverage points'") VLCOVGEN_ITEM("'name':'hier', 'short':'h', 'group':0, 'default':'', 'descr':'Hierarchy path name for the item'") VLCOVGEN_ITEM("'name':'lineno', 'short':'l', 'group':0, 'default':0, 'descr':'Line number for the item'") VLCOVGEN_ITEM("'name':'weight', 'short':'w', 'group':0, 'default':None, 'descr':'For totaling items, weight of this item'") @@ -49,6 +53,10 @@ VLCOVGEN_ITEM("'name':'weight', 'short':'w', 'group':0, 'default':None, 'd #define VL_CIK_COLUMN "n" #define VL_CIK_COMMENT "o" #define VL_CIK_FILENAME "f" +#define VL_CIK_FSM_FROM "Ff" +#define VL_CIK_FSM_TAG "Fg" +#define VL_CIK_FSM_TO "Ft" +#define VL_CIK_FSM_VAR "Fv" #define VL_CIK_HIER "h" #define VL_CIK_LINENO "l" #define VL_CIK_LINESCOV "S" @@ -70,6 +78,10 @@ public: if (key == "column") return VL_CIK_COLUMN; if (key == "comment") return VL_CIK_COMMENT; if (key == "filename") return VL_CIK_FILENAME; + if (key == "fsm_from") return VL_CIK_FSM_FROM; + if (key == "fsm_tag") return VL_CIK_FSM_TAG; + if (key == "fsm_to") return VL_CIK_FSM_TO; + if (key == "fsm_var") return VL_CIK_FSM_VAR; if (key == "hier") return VL_CIK_HIER; if (key == "lineno") return VL_CIK_LINENO; if (key == "linescov") return VL_CIK_LINESCOV; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 00d6d9b92..71f416129 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -100,6 +100,7 @@ set(HEADERS V3File.h V3FileLine.h V3Force.h + V3FsmDetect.h V3Fork.h V3FuncOpt.h V3FunctionTraits.h @@ -277,6 +278,7 @@ set(COMMON_SOURCES V3File.cpp V3FileLine.cpp V3Force.cpp + V3FsmDetect.cpp V3Fork.cpp V3FuncOpt.cpp V3Gate.cpp diff --git a/src/Makefile_obj.in b/src/Makefile_obj.in index a3441cae2..5cf0d2eaa 100644 --- a/src/Makefile_obj.in +++ b/src/Makefile_obj.in @@ -280,6 +280,7 @@ RAW_OBJS_PCH_ASTNOMT = \ V3ExecGraph.o \ V3Expand.o \ V3Force.o \ + V3FsmDetect.o \ V3Fork.o \ V3Gate.o \ V3HierBlock.o \ diff --git a/src/V3AstAttr.h b/src/V3AstAttr.h index fb453d5d6..25962098e 100644 --- a/src/V3AstAttr.h +++ b/src/V3AstAttr.h @@ -311,6 +311,9 @@ public: // VAR_BASE, // V3LinkResolve creates for AstPreSel, V3LinkParam removes VAR_FORCEABLE, // V3LinkParse moves to AstVar::isForceable + VAR_FSM_ARC_INCLUDE_COND, // V3LinkParse moves to AstVar::attrFsmArcInclCond + VAR_FSM_RESET_ARC, // V3LinkParse moves to AstVar::attrFsmResetArc + VAR_FSM_STATE, // V3LinkParse moves to AstVar::attrFsmState VAR_PORT_DTYPE, // V3LinkDot for V3Width to check port dtype VAR_PUBLIC, // V3LinkParse moves to AstVar::sigPublic VAR_PUBLIC_FLAT, // V3LinkParse moves to AstVar::sigPublic @@ -336,10 +339,10 @@ public: "ENUM_NEXT", "ENUM_PREV", "ENUM_NAME", "ENUM_VALID", "FUNC_ARG_PROTO", "FUNC_RETURN_PROTO", "TYPEID", "TYPENAME", - "VAR_BASE", "VAR_FORCEABLE", "VAR_PORT_DTYPE", "VAR_PUBLIC", - "VAR_PUBLIC_FLAT", "VAR_PUBLIC_FLAT_RD", "VAR_PUBLIC_FLAT_RW", - "VAR_ISOLATE_ASSIGNMENTS", "VAR_SC_BIGUINT", "VAR_SC_BV", "VAR_SFORMAT", - "VAR_SPLIT_VAR" + "VAR_BASE", "VAR_FORCEABLE", "VAR_FSM_ARC_INCLUDE_COND", "VAR_FSM_RESET_ARC", + "VAR_FSM_STATE", "VAR_PORT_DTYPE", "VAR_PUBLIC", "VAR_PUBLIC_FLAT", + "VAR_PUBLIC_FLAT_RD", "VAR_PUBLIC_FLAT_RW", "VAR_ISOLATE_ASSIGNMENTS", + "VAR_SC_BIGUINT", "VAR_SC_BV", "VAR_SFORMAT", "VAR_SPLIT_VAR" }; // clang-format on return names[m_e]; diff --git a/src/V3AstNodeOther.h b/src/V3AstNodeOther.h index f3511768e..32a38ca0b 100644 --- a/src/V3AstNodeOther.h +++ b/src/V3AstNodeOther.h @@ -1934,6 +1934,9 @@ class AstVar final : public AstNode { bool m_attrIsolateAssign : 1; // User isolate_assignments attribute bool m_attrSFormat : 1; // User sformat attribute bool m_attrSplitVar : 1; // declared with split_var metacomment + bool m_attrFsmState : 1; // declared with fsm_state metacomment + bool m_attrFsmResetArc : 1; // declared with fsm_reset_arc metacomment + bool m_attrFsmArcInclCond : 1; // declared with fsm_arc_include_cond metacomment bool m_fileDescr : 1; // File descriptor bool m_gotNansiType : 1; // Linker saw Non-ANSI type declaration bool m_isConst : 1; // Table contains constant data @@ -1992,6 +1995,9 @@ class AstVar final : public AstNode { m_attrIsolateAssign = false; m_attrSFormat = false; m_attrSplitVar = false; + m_attrFsmState = false; + m_attrFsmResetArc = false; + m_attrFsmArcInclCond = false; m_fileDescr = false; m_gotNansiType = false; m_isConst = false; @@ -2136,6 +2142,9 @@ public: void attrIsolateAssign(bool flag) { m_attrIsolateAssign = flag; } void attrSFormat(bool flag) { m_attrSFormat = flag; } void attrSplitVar(bool flag) { m_attrSplitVar = flag; } + void attrFsmState(bool flag) { m_attrFsmState = flag; } + void attrFsmResetArc(bool flag) { m_attrFsmResetArc = flag; } + void attrFsmArcInclCond(bool flag) { m_attrFsmArcInclCond = flag; } void rand(const VRandAttr flag) { m_rand = flag; } void usedParam(bool flag) { m_usedParam = flag; } void usedLoopIdx(bool flag) { m_usedLoopIdx = flag; } @@ -2299,6 +2308,9 @@ public: bool attrFileDescr() const { return m_fileDescr; } bool attrSFormat() const { return m_attrSFormat; } bool attrSplitVar() const { return m_attrSplitVar; } + bool attrFsmState() const { return m_attrFsmState; } + bool attrFsmResetArc() const { return m_attrFsmResetArc; } + bool attrFsmArcInclCond() const { return m_attrFsmArcInclCond; } bool attrIsolateAssign() const { return m_attrIsolateAssign; } AstIface* sensIfacep() const { return m_sensIfacep; } VRandAttr rand() const { return m_rand; } @@ -2384,12 +2396,22 @@ class AstCoverOtherDecl final : public AstNodeCoverDecl { // Coverage analysis point declaration // Used for other than toggle types of coverage string m_linescov; + string m_fsmVar; + string m_fsmFrom; + string m_fsmTo; + string m_fsmTag; int m_offset; // Offset column numbers to uniq-ify IFs public: AstCoverOtherDecl(FileLine* fl, const string& page, const string& comment, - const string& linescov, int offset) + const string& linescov, int offset, const string& fsmVar = "", + const string& fsmFrom = "", const string& fsmTo = "", + const string& fsmTag = "") : ASTGEN_SUPER_CoverOtherDecl(fl, page, comment) , m_linescov{linescov} + , m_fsmVar{fsmVar} + , m_fsmFrom{fsmFrom} + , m_fsmTo{fsmTo} + , m_fsmTag{fsmTag} , m_offset{offset} {} ASTGEN_MEMBERS_AstCoverOtherDecl; void dump(std::ostream& str) const override; @@ -2397,6 +2419,10 @@ public: int offset() const { return m_offset; } int size() const override { return 1; } const string& linescov() const { return m_linescov; } + const string& fsmVar() const { return m_fsmVar; } + const string& fsmFrom() const { return m_fsmFrom; } + const string& fsmTo() const { return m_fsmTo; } + const string& fsmTag() const { return m_fsmTag; } bool sameNode(const AstNode* samep) const override { const AstCoverOtherDecl* const asamep = VN_DBG_AS(samep, CoverOtherDecl); return AstNodeCoverDecl::sameNode(samep) && linescov() == asamep->linescov(); diff --git a/src/V3AstNodes.cpp b/src/V3AstNodes.cpp index acf4b10fa..510703e4f 100644 --- a/src/V3AstNodes.cpp +++ b/src/V3AstNodes.cpp @@ -3004,6 +3004,9 @@ void AstVar::dump(std::ostream& str) const { if (processQueue()) str << " [PROCQ]"; if (sampled()) str << " [SAMPLED]"; if (attrIsolateAssign()) str << " [aISO]"; + if (attrFsmState()) str << " [aFSMSTATE]"; + if (attrFsmResetArc()) str << " [aFSMRESETARC]"; + if (attrFsmArcInclCond()) str << " [aFSMARCCOND]"; if (attrFileDescr()) str << " [aFD]"; if (isFuncReturn()) { str << " [FUNCRTN]"; @@ -3036,6 +3039,9 @@ void AstVar::dumpJson(std::ostream& str) const { dumpJsonBoolFuncIf(str, processQueue); dumpJsonBoolFuncIf(str, sampled); dumpJsonBoolFuncIf(str, attrIsolateAssign); + dumpJsonBoolFuncIf(str, attrFsmState); + dumpJsonBoolFuncIf(str, attrFsmResetArc); + dumpJsonBoolFuncIf(str, attrFsmArcInclCond); dumpJsonBoolFuncIf(str, attrFileDescr); dumpJsonBoolFuncIf(str, isDpiOpenArray); dumpJsonBoolFuncIf(str, isFuncReturn); @@ -3283,10 +3289,18 @@ void AstNodeCoverDecl::dumpJson(std::ostream& str) const { void AstCoverOtherDecl::dump(std::ostream& str) const { this->AstNodeCoverDecl::dump(str); if (!linescov().empty()) str << " lc=" << linescov(); + if (!fsmVar().empty()) str << " fv=" << fsmVar(); + if (!fsmFrom().empty()) str << " ff=" << fsmFrom(); + if (!fsmTo().empty()) str << " ft=" << fsmTo(); + if (!fsmTag().empty()) str << " fg=" << fsmTag(); } void AstCoverOtherDecl::dumpJson(std::ostream& str) const { this->AstNodeCoverDecl::dumpJson(str); dumpJsonStrFunc(str, linescov); + dumpJsonStrFunc(str, fsmVar); + dumpJsonStrFunc(str, fsmFrom); + dumpJsonStrFunc(str, fsmTo); + dumpJsonStrFunc(str, fsmTag); } void AstCoverToggleDecl::dump(std::ostream& str) const { this->AstNodeCoverDecl::dump(str); diff --git a/src/V3EmitCFunc.h b/src/V3EmitCFunc.h index d9b6bb9e3..1427f096b 100644 --- a/src/V3EmitCFunc.h +++ b/src/V3EmitCFunc.h @@ -826,6 +826,14 @@ public: putsQuoted(VIdProtect::protectWordsIf(nodep->comment(), nodep->protect())); puts(", "); putsQuoted(nodep->linescov()); + puts(", "); + putsQuoted(VIdProtect::protectWordsIf(nodep->fsmVar(), nodep->protect())); + puts(", "); + putsQuoted(VIdProtect::protectWordsIf(nodep->fsmFrom(), nodep->protect())); + puts(", "); + putsQuoted(VIdProtect::protectWordsIf(nodep->fsmTo(), nodep->protect())); + puts(", "); + putsQuoted(VIdProtect::protectWordsIf(nodep->fsmTag(), nodep->protect())); puts(");\n"); } void visit(AstCoverToggleDecl* nodep) override { diff --git a/src/V3EmitCHeaders.cpp b/src/V3EmitCHeaders.cpp index eb1b43d36..7947bb070 100644 --- a/src/V3EmitCHeaders.cpp +++ b/src/V3EmitCHeaders.cpp @@ -197,7 +197,9 @@ class EmitCHeader final : public EmitCConstInit { puts(v3Global.opt.threads() > 1 ? "std::atomic" : "uint32_t"); puts("* countp, bool enable, const char* filenamep, int lineno, int column,\n"); puts("const char* hierp, const char* pagep, const char* commentp, const char* " - "linescovp);\n"); + "linescovp,\n"); + puts("const char* fsmVarp, const char* fsmFromp, const char* fsmTop, const char* " + "fsmTagp);\n"); } if (v3Global.opt.coverageToggle() && !VN_IS(modp, Class)) { diff --git a/src/V3EmitCImp.cpp b/src/V3EmitCImp.cpp index eb7eb4718..ad955cd3b 100644 --- a/src/V3EmitCImp.cpp +++ b/src/V3EmitCImp.cpp @@ -180,7 +180,9 @@ class EmitCImp final : public EmitCFunc { puts(v3Global.opt.threads() > 1 ? "std::atomic" : "uint32_t"); puts("* countp, bool enable, const char* filenamep, int lineno, int column,\n"); puts("const char* hierp, const char* pagep, const char* commentp, const char* " - "linescovp) {\n"); + "linescovp,\n"); + puts("const char* fsmVarp, const char* fsmFromp, const char* fsmTop, const char* " + "fsmTagp) {\n"); if (v3Global.opt.threads() > 1) { puts("assert(sizeof(uint32_t) == sizeof(std::atomic));\n"); puts("uint32_t* count32p = reinterpret_cast(countp);\n"); @@ -198,10 +200,14 @@ class EmitCImp final : public EmitCFunc { puts(" \"filename\",filenamep,"); puts(" \"lineno\",lineno,"); puts(" \"column\",column,\n"); - puts("\"hier\",fullhier,"); + puts("\"hier\",fullhier.c_str(),"); puts(" \"page\",pagep,"); puts(" \"comment\",commentp,"); - puts(" (linescovp[0] ? \"linescov\" : \"\"), linescovp);\n"); + puts(" (linescovp[0] ? \"linescov\" : \"\"), linescovp,"); + puts(" (fsmVarp[0] ? \"fsm_var\" : \"\"), fsmVarp,"); + puts(" (fsmFromp[0] ? \"fsm_from\" : \"\"), fsmFromp,"); + puts(" (fsmTop[0] ? \"fsm_to\" : \"\"), fsmTop,"); + puts(" (fsmTagp[0] ? \"fsm_tag\" : \"\"), fsmTagp);\n"); puts("}\n"); } if (v3Global.opt.coverageToggle()) { @@ -237,7 +243,7 @@ class EmitCImp final : public EmitCFunc { puts(" \"filename\",filenamep,"); puts(" \"lineno\",lineno,"); puts(" \"column\",column,\n"); - puts("\"hier\",fullhier,"); + puts("\"hier\",fullhier.c_str(),"); puts(" \"page\",pagep,"); puts(" \"comment\",commentWithIndex.c_str(),"); puts(" \"\", \"\");\n"); // linescov argument, but in toggle coverage it is always diff --git a/src/V3Error.h b/src/V3Error.h index c5f49dfd4..b36e8451a 100644 --- a/src/V3Error.h +++ b/src/V3Error.h @@ -106,6 +106,7 @@ public: ENUMITEMWIDTH, // Error: enum item width mismatch ENUMVALUE, // Error: enum type needs explicit cast EOFNEWLINE, // End-of-file missing newline + FSMMULTI, // Multiple FSM candidates in one always block FUNCTIMECTL, // Functions cannot have timing/delay/wait FUTURE, // Feature is under development and not yet supported GENCLK, // Generated Clock. Historical, never issued. @@ -223,16 +224,16 @@ public: "BSSPACE", "CASEINCOMPLETE", "CASEOVERLAP", "CASEWITHX", "CASEX", "CASTCONST", "CDCRSTLOGIC", "CLKDATA", "CMPCONST", "COLONPLUS", "COMBDLY", "CONSTRAINTIGN", "CONTASSREG", "COVERIGN", "DECLFILENAME", "DEFOVERRIDE", "DEFPARAM", "DEPRECATED", - "ENCAPSULATED", "ENDLABEL", "ENUMITEMWIDTH", "ENUMVALUE", "EOFNEWLINE", "FUNCTIMECTL", - "FUTURE", "GENCLK", "GENUNNAMED", "HIERBLOCK", "HIERPARAM", "IFDEPTH", "IGNOREDRETURN", - "IMPERFECTSCH", "IMPLICIT", "IMPLICITSTATIC", "IMPORTSTAR", "IMPURE", "INCABSPATH", - "INFINITELOOP", "INITIALDLY", "INSECURE", "INSIDETRUE", "LATCH", "LITENDIAN", - "MINTYPMAXDLY", "MISINDENT", "MODDUP", "MODMISSING", "MULTIDRIVEN", "MULTITOP", - "NEWERSTD", "NOEFFECT", "NOLATCH", "NONSTD", "NORETURN", "NULLPORT", "PARAMNODEFAULT", - "PINCONNECTEMPTY", "PINMISSING", "PINNOCONNECT", "PINNOTFOUND", "PKGNODECL", - "PREPROCZERO", "PROCASSINIT", "PROCASSWIRE", "PROFOUTOFDATE", "PROTECTED", + "ENCAPSULATED", "ENDLABEL", "ENUMITEMWIDTH", "ENUMVALUE", "EOFNEWLINE", "FSMMULTI", + "FUNCTIMECTL", "FUTURE", "GENCLK", "GENUNNAMED", "HIERBLOCK", "HIERPARAM", "IFDEPTH", + "IGNOREDRETURN", "IMPERFECTSCH", "IMPLICIT", "IMPLICITSTATIC", "IMPORTSTAR", "IMPURE", + "INCABSPATH","INFINITELOOP", "INITIALDLY", "INSECURE", "INSIDETRUE", "LATCH", + "LITENDIAN", "MINTYPMAXDLY", "MISINDENT", "MODDUP", "MODMISSING", "MULTIDRIVEN", + "MULTITOP", "NEWERSTD", "NOEFFECT", "NOLATCH", "NONSTD", "NORETURN", "NULLPORT", + "PARAMNODEFAULT", "PINCONNECTEMPTY", "PINMISSING", "PINNOCONNECT", "PINNOTFOUND", + "PKGNODECL", "PREPROCZERO", "PROCASSINIT", "PROCASSWIRE", "PROFOUTOFDATE", "PROTECTED", "PROTOTYPEMIS", "RANDC", "REALCVT", "REDEFMACRO", "RISEFALLDLY", "SELRANGE", - "SHORTREAL", "SIDEEFFECT", "SPECIFYIGN", "SPLITVAR", "STATICVAR", "STMTDLY", + "SHORTREAL", "SIDEEFFECT", "SPECIFYIGN", "SPLITVAR", "STATICVAR","STMTDLY", "SUPERNFIRST", "SYMRSVDWORD", "SYNCASYNCNET", "TICKCOUNT", "TIMESCALEMOD", "UNDRIVEN", "UNOPT", "UNOPTFLAT", "UNOPTTHREADS", "UNPACKED", "UNSATCONSTR", "UNSIGNED", "UNUSED", "UNUSEDGENVAR", "UNUSEDLOOP", "UNUSEDPARAM", "UNUSEDSIGNAL", "USERERROR", "USERFATAL", diff --git a/src/V3FsmDetect.cpp b/src/V3FsmDetect.cpp new file mode 100644 index 000000000..205c22505 --- /dev/null +++ b/src/V3FsmDetect.cpp @@ -0,0 +1,785 @@ +// -*- mode: C++; c-file-style: "cc-mode" -*- +//************************************************************************* +// DESCRIPTION: Verilator: FSM coverage detect pass +// +// Code available from: https://verilator.org +// +//************************************************************************* +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of either the GNU Lesser General Public License Version 3 +// or the Perl Artistic License Version 2.0. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 +// +//************************************************************************* +// FSM COVERAGE DETECT: +// Walk clocked always blocks while the original FSM structure is still +// present, build a per-FSM V3Graph representation of the extracted +// states/transitions, then immediately lower that completed graph state +// into the final coverage declarations, previous-state tracking, and +// active blocks needed to implement FSM state and arc coverage in the +// generated model. +// +//************************************************************************* + +#include "V3PchAstNoMT.h" + +#include "V3FsmDetect.h" + +#include "V3Ast.h" +#include "V3Graph.h" + +#include +#include +#include +#include + +VL_DEFINE_DEBUG_FUNCTIONS; + +namespace { + +// Captures one sensitivity-list entry so the lowering phase can later rebuild +// an active block with the same triggering event control. +struct FsmSenDesc final { + // Encoded edge kind copied from AstSenItem::edgeType() so lowering can + // rebuild the same trigger semantics on the synthesized coverage block. + VEdgeType::en edgeType = static_cast(0); + // Triggering signal in the saved scoped AST. + AstVarScope* varScopep = nullptr; +}; + +// Captures the simple reset predicate shape that survives to this pass after +// earlier normalization so reset arcs can be reconstructed during lowering. +struct FsmResetCondDesc final { + // Reset signal used by the FSM in the saved scoped AST. + AstVarScope* varScopep = nullptr; +}; + +class FsmGraph; + +class FsmVertex VL_NOT_FINAL : public V3GraphVertex { + VL_RTTI_IMPL(FsmVertex, V3GraphVertex) + +public: + enum class Kind : uint8_t { STATE, RESET_ANY, DEFAULT_ANY }; + +private: + Kind m_kind; // State vs synthetic ANY/default vertex role. + string m_label; // User-facing state or pseudo-state label. + int m_value = 0; // Encoded state value for real state vertices. + +protected: + FsmVertex(V3Graph* graphp, Kind kind, string label, int value) VL_MT_DISABLED + : V3GraphVertex{graphp} + , m_kind{kind} + , m_label{label} + , m_value{value} {} + ~FsmVertex() override = default; + +public: + Kind kind() const { return m_kind; } + bool isState() const { return m_kind == Kind::STATE; } + bool isResetAny() const { return m_kind == Kind::RESET_ANY; } + bool isDefaultAny() const { return m_kind == Kind::DEFAULT_ANY; } + const string& label() const { return m_label; } + int value() const { return m_value; } + + string name() const override VL_MT_SAFE { return m_label + "=" + cvtToStr(m_value); } +}; + +class FsmStateVertex final : public FsmVertex { + VL_RTTI_IMPL(FsmStateVertex, FsmVertex) + +public: + FsmStateVertex(V3Graph* graphp, string label, int value) VL_MT_DISABLED + : FsmVertex{graphp, Kind::STATE, label, value} {} + ~FsmStateVertex() override = default; + + string dotColor() const override { return "lightblue"; } + string dotShape() const override { return "ellipse"; } +}; + +class FsmPseudoVertex final : public FsmVertex { + VL_RTTI_IMPL(FsmPseudoVertex, FsmVertex) + +public: + FsmPseudoVertex(V3Graph* graphp, Kind kind, string label) VL_MT_DISABLED + : FsmVertex{graphp, kind, label, 0} {} + ~FsmPseudoVertex() override = default; + + string name() const override VL_MT_SAFE { return label(); } + string dotColor() const override { return isResetAny() ? "darkgreen" : "orange"; } + string dotShape() const override { return "diamond"; } +}; + +class FsmArcEdge final : public V3GraphEdge { + VL_RTTI_IMPL(FsmArcEdge, V3GraphEdge) + bool m_isReset = false; // Arc originates from the synthetic reset source. + bool m_isCond = false; // Arc came from a conditional next-state split. + bool m_isDefault = false; // Arc represents a case default source. + FileLine* m_flp = nullptr; // Source location for emitted coverage metadata. + +public: + FsmArcEdge(V3Graph* graphp, FsmVertex* fromp, FsmStateVertex* top, bool isReset, + bool isCond, bool isDefault, FileLine* flp) VL_MT_DISABLED + : V3GraphEdge{graphp, fromp, top, 1} + , m_isReset{isReset} + , m_isCond{isCond} + , m_isDefault{isDefault} + , m_flp{flp} {} + ~FsmArcEdge() override = default; + + bool isReset() const { return m_isReset; } + bool isCond() const { return m_isCond; } + bool isDefault() const { return m_isDefault; } + FileLine* fileline() const { return m_flp; } + + string dotLabel() const override { + if (m_isReset) return "reset"; + if (m_isDefault) return "default"; + if (m_isCond) return "cond"; + return ""; + } + string dotColor() const override { + if (m_isReset) return "darkgreen"; + if (m_isDefault) return "orange"; + if (m_isCond) return "blue"; + return "black"; + } +}; + +// One graph per detected FSM. Graph-level metadata captures the non-graph +// context needed to lower states/arcs back into the AST after detection. +class FsmGraph final : public V3Graph { + AstScope* m_scopep = nullptr; // Owning scoped block for the detected FSM. + AstAlways* m_alwaysp = nullptr; // Original always block being instrumented. + string m_stateVarName; // Pretty state variable name for user-visible output. + string m_stateVarInternalName; // Internal state symbol name for dump tags. + AstVarScope* m_stateVarScopep = nullptr; // Scoped state variable being tracked. + std::vector m_senses; // Saved event controls for recreated active blocks. + FsmResetCondDesc m_resetCond; // Saved reset predicate shape, if one exists. + bool m_hasResetCond = false; // Whether the detected FSM had a reset branch. + bool m_resetInclude = false; // Whether reset arcs count toward coverage totals. + bool m_inclCond = false; // Whether conditional arcs should be kept explicitly. + FileLine* m_flp = nullptr; // Representative source location for declarations/arcs. + std::unordered_map m_stateVertices; // Value to state-vertex map. + FsmPseudoVertex* m_resetVertexp = nullptr; // Synthetic ANY source for reset arcs. + FsmPseudoVertex* m_defaultVertexp = nullptr; // Synthetic default source for case defaults. + +public: + FsmGraph() VL_MT_DISABLED + : m_resetVertexp{new FsmPseudoVertex{this, FsmVertex::Kind::RESET_ANY, "ANY"}} + , m_defaultVertexp{new FsmPseudoVertex{this, FsmVertex::Kind::DEFAULT_ANY, "default"}} {} + + AstScope* scopep() const { return m_scopep; } + void scopep(AstScope* scopep) { m_scopep = scopep; } + AstAlways* alwaysp() const { return m_alwaysp; } + void alwaysp(AstAlways* alwaysp) { m_alwaysp = alwaysp; } + const string& stateVarName() const { return m_stateVarName; } + void stateVarName(const string& name) { m_stateVarName = name; } + const string& stateVarInternalName() const { return m_stateVarInternalName; } + void stateVarInternalName(const string& name) { m_stateVarInternalName = name; } + AstVarScope* stateVarScopep() const { return m_stateVarScopep; } + void stateVarScopep(AstVarScope* vscp) { m_stateVarScopep = vscp; } + const std::vector& senses() const { return m_senses; } + std::vector& senses() { return m_senses; } + const FsmResetCondDesc& resetCond() const { return m_resetCond; } + FsmResetCondDesc& resetCond() { return m_resetCond; } + bool hasResetCond() const { return m_hasResetCond; } + void hasResetCond(bool flag) { m_hasResetCond = flag; } + bool resetInclude() const { return m_resetInclude; } + void resetInclude(bool flag) { m_resetInclude = flag; } + bool inclCond() const { return m_inclCond; } + void inclCond(bool flag) { m_inclCond = flag; } + FileLine* fileline() const { return m_flp; } + void fileline(FileLine* flp) { m_flp = flp; } + + FsmStateVertex* addStateVertex(string label, int value) VL_MT_DISABLED { + FsmStateVertex* const vertexp = new FsmStateVertex{this, label, value}; + m_stateVertices.emplace(value, vertexp); + return vertexp; + } + FsmPseudoVertex* resetAnyVertex() VL_MT_DISABLED { return m_resetVertexp; } + FsmPseudoVertex* defaultAnyVertex() VL_MT_DISABLED { return m_defaultVertexp; } + FsmArcEdge* addArc(int fromValue, int toValue, bool isReset, bool isCond, bool isDefault, + FileLine* flp) VL_MT_DISABLED { + FsmStateVertex* const top = m_stateVertices.at(toValue); + FsmVertex* fromp = nullptr; + if (isReset) { + fromp = resetAnyVertex(); + } else if (isDefault) { + fromp = defaultAnyVertex(); + } else { + fromp = m_stateVertices.at(fromValue); + } + return new FsmArcEdge{this, fromp, top, isReset, isCond, isDefault, flp}; + } + + string name() const VL_MT_SAFE { + return "FSM " + + (m_stateVarName.empty() ? (m_stateVarScopep ? m_stateVarScopep->name() : "") + : m_stateVarName); + } + string dumpTag(size_t index) const { + string tag = stateVarInternalName(); + for (char& ch : tag) { + if (!std::isalnum(static_cast(ch))) ch = '_'; + } + return "fsm_" + cvtToStr(index) + "_" + tag; + } +}; + +struct DetectedFsm final { + std::unique_ptr graphp; // Extracted graph for one detected FSM candidate. +}; +using DetectedFsmMap = std::map; + +// Local shared state between the two adjacent FSM coverage phases. Detection +// fills this with recovered FSM graphs; lowering consumes the completed graphs +// immediately afterward without needing any AST serialization bridge. +class FsmState final { + // All detected FSMs keyed by state varscope name. This is the only bridge + // between the adjacent detect and lower phases, so the second phase never + // needs to rediscover or serialize the extracted machine. + DetectedFsmMap m_fsms; + +public: + DetectedFsmMap& fsms() { return m_fsms; } + const DetectedFsmMap& fsms() const { return m_fsms; } +}; + +// Detection runs while the original clocked/case structure is still intact and +// populates graph-backed FSM models without mutating the tree mid-traversal. +// This pass is intentionally conservative: for this PR we only lock down the +// small set of transition/selector forms that are already stable in the +// normalized AST we see here. The remaining reject branches are therefore +// mostly future-feature boundaries, not accidental dead code. +class FsmDetectVisitor final : public VNVisitor { + // STATE - for current visit position (use VL_RESTORER) + FsmState& m_state; + AstScope* m_scopep = nullptr; + + // METHODS + // Enum-backed FSMs may be wrapped in refs/typedefs; normalize to the + // underlying enum type before deciding whether a case is a candidate. + static AstNodeDType* unwrapEnumCandidate(AstNodeDType* dtypep) { + return dtypep->skipRefToEnump(); + } + + // Reset arcs are only modeled for the simple signal form that survives to + // this pass after earlier normalization. + static bool isSimpleResetCond(AstNodeExpr* condp) { + return VN_IS(condp, VarRef); + } + + // Normalize the reset condition into a compact description so the lowering + // phase can regenerate the same predicate after detection. By the time + // this pass runs, active-low source forms such as "!rst_n" have already + // been canonicalized to a positive-condition if/else shape, so only a + // plain VarRef survives here. + static FsmResetCondDesc describeResetCond(AstNodeExpr* condp) { + FsmResetCondDesc desc; + if (AstVarRef* const vrefp = VN_CAST(condp, VarRef)) { + desc.varScopep = vrefp->varScopep(); + } + return desc; + } + + // Snapshot the original event control so the lowering phase can rebuild an + // active block with the same edge semantics. + static std::vector describeSenTree(AstSenTree* sentreep) { + std::vector senses; + for (AstSenItem* itemp = sentreep->sensesp(); itemp; + itemp = VN_AS(itemp->nextp(), SenItem)) { + AstNodeVarRef* const vrefp = itemp->varrefp(); + if (!vrefp) continue; + FsmSenDesc desc; + desc.edgeType = itemp->edgeType().m_e; + desc.varScopep = vrefp->varScopep(); + senses.push_back(desc); + } + return senses; + } + + // Ignore existing coverage increments so FSM detection sees the user logic + // rather than other instrumentation already attached to the block. + static bool isIgnorableStmt(AstNode* nodep) { return VN_IS(nodep, CoverInc); } + + // Conservative extractor: only treat a branch as simple when exactly one + // non-coverage statement remains after unwrapping. Richer multi-statement + // or control-flow forms are intentionally left for follow-on FSM-detection + // work instead of being partially inferred here. + static AstNode* singleMeaningfulStmt(AstNode* stmtp) { + AstNode* resultp = nullptr; + for (AstNode* nodep = stmtp; nodep; nodep = nodep->nextp()) { + if (isIgnorableStmt(nodep)) continue; + if (resultp) return nullptr; + resultp = nodep; + } + return resultp; + } + + // Recognize the direct "state <= X" form that gives us an unambiguous arc + // target without needing deeper control-flow reasoning. Branches that fall + // out here represent currently unsupported next-state shapes rather than + // bugs in the implemented subset. + static AstNodeAssign* directStateAssign(AstNode* stmtp, AstVarScope* stateVscp) { + AstNode* const nodep = singleMeaningfulStmt(stmtp); + if (!nodep) return nullptr; + AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign); + if (!assp) return nullptr; + AstVarRef* const vrefp = VN_CAST(assp->lhsp(), VarRef); + if (!vrefp || vrefp->varScopep() != stateVscp) return nullptr; + return assp; + } + + // Prefer enum labels in reports; fall back to synthetic labels for forced + // non-enum FSMs so coverage points remain human-readable. + static string labelForValue(const std::unordered_map& labels, int value) { + const std::unordered_map::const_iterator it = labels.find(value); + return it == labels.end() ? ("S" + cvtToStr(value)) : it->second; + } + + // The extractor only models constant-valued state transitions, and by the + // time detect runs those values have already been constant-folded. + static bool exprConstValue(AstNodeExpr* exprp, int& value) { + if (AstConst* const constp = VN_CAST(exprp, Const)) { + value = constp->toSInt(); + return true; + } + return false; + } + + // Enum-backed FSMs should only transition to values that were interned as + // known states. If a constant transition targets some other encoding, warn + // and skip FSM instrumentation for that edge rather than silently dropping + // it or turning optional coverage into a hard compile failure. + static bool validateKnownStateValue(AstNode* nodep, + const std::unordered_map& labels, int value) { + if (labels.find(value) != labels.end()) return true; + nodep->v3warn(COVERIGN, + "Ignoring unsupported: FSM coverage on enum state transitions " + "that assign a constant not present in the declared enum"); + return false; + } + + // Extract supported case-item transitions in one place so the conservative + // policy for direct and ternary forms stays consistent. The false exits in + // this helper are deliberate subset boundaries: they document shapes we do + // not yet model in this PR and that future FSM-detection work may widen. + static bool emitCaseItemArcs(FsmGraph& graph, AstCaseItem* itemp, AstVarScope* stateVscp, + const std::unordered_map& labels, bool inclCond) { + std::vector> froms; + if (itemp->isDefault()) { + if (!inclCond) return false; + froms.emplace_back("default", 0); + } else { + for (AstNodeExpr* condp = itemp->condsp(); condp; + condp = VN_CAST(condp->nextp(), NodeExpr)) { + int value = 0; + if (!exprConstValue(condp, value)) continue; + froms.emplace_back(labelForValue(labels, value), value); + } + if (froms.empty()) return false; + } + + if (AstNodeAssign* const assp = directStateAssign(itemp->stmtsp(), stateVscp)) { + int toValue = 0; + if (exprConstValue(assp->rhsp(), toValue)) { + if (!validateKnownStateValue(assp, labels, toValue)) return true; + for (const std::pair& from : froms) { + graph.addArc(from.second, toValue, false, false, itemp->isDefault(), + assp->fileline()); + } + return true; + } + + if (AstCond* const condp = VN_CAST(assp->rhsp(), Cond)) { + int thenValue = 0; + int elseValue = 0; + const bool simpleCond = exprConstValue(condp->thenp(), thenValue) + && exprConstValue(condp->elsep(), elseValue); + if (simpleCond || inclCond) { + if (!validateKnownStateValue(condp->thenp(), labels, thenValue)) return true; + if (!validateKnownStateValue(condp->elsep(), labels, elseValue)) return true; + for (const int branchValue : {thenValue, elseValue}) { + for (const std::pair& from : froms) { + graph.addArc(from.second, branchValue, false, true, + itemp->isDefault(), assp->fileline()); + } + } + return true; + } + } + } + + return false; + } + + // Reset transitions are described separately because they live in the reset + // branch outside the steady-state case statement. + static void addResetArcs(FsmGraph& graph, AstNode* stmtsp, AstVarScope* stateVscp, + const std::unordered_map& labels) { + for (AstNode* nodep = stmtsp; nodep; nodep = nodep->nextp()) { + if (AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign)) { + AstVarRef* const vrefp = VN_CAST(assp->lhsp(), VarRef); + int toValue = 0; + if (vrefp && vrefp->varScopep() == stateVscp && exprConstValue(assp->rhsp(), toValue)) { + if (!validateKnownStateValue(assp, labels, toValue)) continue; + graph.addArc(0, toValue, true, false, false, assp->fileline()); + } + } + } + } + + // Turn one candidate case statement into the graph representation that the + // later lowering phase will consume directly, while reviewers can still + // inspect the extracted machine via DOT dumps. + void processCase(AstCase* casep, AstNodeExpr* resetCondp, AstAlways* alwaysp) { + AstVarRef* const selp = VN_CAST(casep->exprp(), VarRef); + if (!selp) return; + AstVarScope* const stateVscp = selp->varScopep(); + AstVar* const stateVarp = selp->varp(); + AstEnumDType* enump = VN_CAST(unwrapEnumCandidate(stateVscp->dtypep()), EnumDType); + if (!enump) enump = VN_CAST(unwrapEnumCandidate(stateVarp->dtypep()), EnumDType); + const bool forced = stateVarp->attrFsmState(); + if (!enump && !forced) return; + + std::vector> 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 (!entry.graphp) { + entry.graphp.reset(new FsmGraph{}); + entry.graphp->scopep(m_scopep); + entry.graphp->alwaysp(alwaysp); + entry.graphp->stateVarName(stateVscp->prettyName()); + entry.graphp->stateVarInternalName(stateVarp->name()); + entry.graphp->stateVarScopep(stateVscp); + entry.graphp->senses() = describeSenTree(alwaysp->sentreep()); + entry.graphp->resetCond() = describeResetCond(resetCondp); + entry.graphp->hasResetCond(entry.graphp->resetCond().varScopep != nullptr); + entry.graphp->resetInclude(stateVarp->attrFsmResetArc()); + entry.graphp->inclCond(stateVarp->attrFsmArcInclCond()); + entry.graphp->fileline(casep->fileline()); + for (const std::pair& state : states) { + entry.graphp->addStateVertex(state.first, state.second); + } + } + for (AstCaseItem* itemp = casep->itemsp(); itemp; itemp = VN_AS(itemp->nextp(), CaseItem)) { + emitCaseItemArcs(*entry.graphp, itemp, stateVscp, labels, entry.graphp->inclCond()); + } + } + + // Find the first supported FSM candidate in a clocked always block, warn on + // additional candidates, and attach reset arcs when present. Candidate + // filtering stays narrow on purpose: we prefer to skip ambiguous shapes now + // and expand detection in a later PR rather than over-infer coverage from + // forms we do not yet model confidently. + void processAlways(AstAlways* alwaysp) { + if (!alwaysp->sentreep() || !alwaysp->sentreep()->hasClocked()) return; + std::vector> candidates; + AstNode* stmtsp = alwaysp->stmtsp(); + AstIf* const firstIfp = VN_CAST(stmtsp, If); + if (firstIfp) { + if (AstCase* const casep = VN_CAST(firstIfp->elsesp(), Case)) { + candidates.emplace_back(casep, isSimpleResetCond(firstIfp->condp()) ? firstIfp->condp() + : nullptr); + } + } + for (AstNode* nodep = stmtsp; nodep; nodep = nodep->nextp()) { + if (AstCase* const casep = VN_CAST(nodep, Case)) candidates.emplace_back(casep, nullptr); + } + if (candidates.empty()) return; + + AstVarScope* firstVscp = nullptr; + for (const std::pair& cand : candidates) { + AstVarRef* const selp = VN_CAST(cand.first->exprp(), VarRef); + AstVarScope* const vscp = selp ? selp->varScopep() : nullptr; + if (!vscp) continue; + if (!firstVscp) { + firstVscp = vscp; + processCase(cand.first, cand.second, alwaysp); + } else if (vscp != firstVscp) { + cand.first->v3warn(FSMMULTI, + "FSM coverage: multiple enum-typed case statements found in " + "the same always block. Only the first candidate will be " + "instrumented."); + } else { + cand.first->v3warn( + COVERIGN, + "Ignoring unsupported: FSM coverage on multiple supported case " + "statements found in the same always block. Only the first " + "candidate will be instrumented."); + } + } + + if (!(firstIfp && firstVscp)) return; + const DetectedFsmMap& fsms = m_state.fsms(); + const DetectedFsmMap::const_iterator it = fsms.find(firstVscp->name()); + if (it == fsms.end()) return; + FsmGraph* const graphp = it->second.graphp.get(); + if (!graphp->hasResetCond()) return; + std::unordered_map labels; + for (const V3GraphVertex& vtx : graphp->vertices()) { + const FsmVertex* const vertexp = vtx.as(); + if (!vertexp->isState()) continue; + labels.emplace(vertexp->value(), vertexp->label()); + } + addResetArcs(*graphp, firstIfp->thensp(), firstVscp, labels); + } + + // Track the current scope so each detected FSM records the module/scope + // where instrumentation must later be inserted. + void visit(AstScope* nodep) override { + VL_RESTORER(m_scopep); + m_scopep = nodep; + iterateChildren(nodep); + } + + // FSM extraction only cares about clocked always processes. + void visit(AstAlways* nodep) override { processAlways(nodep); } + + // Continue the walk through the rest of the design hierarchy. + void visit(AstNode* nodep) override { iterateChildren(nodep); } + +public: + // CONSTRUCTORS + // Collect all FSM graphs into the shared local state before the lowering + // phase starts mutating the AST with coverage machinery. + FsmDetectVisitor(FsmState& state, AstNetlist* rootp) + : m_state{state} { + iterate(rootp); + } +}; + +// Lower the completed FSM graphs into the concrete coverage declarations, +// previous-state tracking, and pre/post-triggered instrumentation that the +// runtime uses to record state and transition coverage. +class FsmLowerVisitor final { + // STATE - across all visitors + const FsmState& m_state; + + // METHODS + // Rebuild a state-typed constant using the tracked state variable + // width/sign so emitted comparisons match the original representation. + static AstConst* makeStateConst(FileLine* flp, AstVarScope* vscp, int value) { + V3Number num{flp, vscp->width(), static_cast(value)}; + num.isSigned(vscp->dtypep()->isSigned()); + return new AstConst{flp, num}; + } + + // Build guards incrementally without forcing callers to special-case the + // first predicate; this keeps emitted state/arc conditions readable. + static AstNodeExpr* andExpr(FileLine* flp, AstNodeExpr* lhsp, AstNodeExpr* rhsp) { + if (!lhsp) return rhsp; + return new AstLogAnd{flp, lhsp, rhsp}; + } + + static AstNodeExpr* buildResetCond(FileLine* flp, AstVarScope* resetVscp, + const FsmResetCondDesc&) { + return new AstVarRef{flp, resetVscp, VAccess::READ}; + } + + // Rebuild the original event control from the saved sense description so + // post-state coverage sampling runs on the same triggering edges. + static AstSenTree* buildSenTree( + FileLine* flp, const std::vector& senses) { + AstSenTree* const sentreep = new AstSenTree{flp, nullptr}; + for (const FsmSenDesc& sense : senses) { + AstSenItem* const senItemp = new AstSenItem{ + flp, VEdgeType{sense.edgeType}, + new AstVarRef{flp, sense.varScopep, VAccess::READ}}; + sentreep->addSensesp(senItemp); + } + return sentreep; + } + + // Lower one fully detected FSM graph into the concrete coverage machinery + // used by generated models: declarations, previous-state tracking, and the + // pre/post-triggered increment logic for states and arcs. + void buildOne(const FsmGraph& graph) { + AstAlways* const alwaysp = graph.alwaysp(); + AstScope* const scopep = graph.scopep(); + AstVarScope* const stateVscp = graph.stateVarScopep(); + FileLine* const flp = graph.fileline(); + AstNodeModule* const modp = scopep->modp(); + AstNodeDType* const prevDTypep + = scopep->findLogicDType(stateVscp->width(), stateVscp->width(), + stateVscp->dtypep()->numeric()); + AstVarScope* const prevVscp + = scopep->createTemp("__Vfsmcov_prev__" + stateVscp->varp()->shortName(), prevDTypep); + // The saved previous-state temp crosses the scheduler's pre/post split + // in the same way as Verilator's built-in NBA shadow variables, so keep + // both vars marked as post-life participants for stable MT ordering. + stateVscp->optimizeLifePost(true); + prevVscp->optimizeLifePost(true); + + AstActive* const initActivep + = new AstActive{flp, "fsm-coverage-init", + new AstSenTree{flp, new AstSenItem{flp, AstSenItem::Initial{}}}}; + initActivep->senTreeStorep(initActivep->sentreep()); + // Seed the previous-state temp during initialization so the first + // clock edge compares against a defined state value. + initActivep->addStmtsp(new AstInitialStatic{ + flp, new AstAssign{flp, new AstVarRef{flp, prevVscp, VAccess::WRITE}, + new AstVarRef{flp, stateVscp, VAccess::READ}}}); + scopep->addBlocksp(initActivep); + + AstAlwaysPost* const covPostp = new AstAlwaysPost{flp}; + // Save the previous state as plain sequential logic at the front of + // the original always_ff body, then evaluate coverage in post logic + // after the delayed state update commits. This avoids a scheduler race + // between a separate AstAlwaysPre task and the real state commit. + AstNode* const bodysp = alwaysp->stmtsp()->unlinkFrBackWithNext(); + alwaysp->addStmtsp(new AstAssign{flp, new AstVarRef{flp, prevVscp, VAccess::WRITE}, + new AstVarRef{flp, stateVscp, VAccess::READ}}); + alwaysp->addStmtsp(bodysp); + + for (const V3GraphVertex& vtx : graph.vertices()) { + const FsmVertex* const vertexp = vtx.as(); + if (!vertexp->isState()) continue; + const FsmStateVertex* const statep = vtx.as(); + // State coverage fires when the FSM enters a state from any other + // value, so repeated self-holds do not count as new entries. + AstCoverOtherDecl* const declp = new AstCoverOtherDecl{ + flp, "v_fsm_state/" + modp->prettyName(), + graph.stateVarName() + "::" + statep->label(), "", 0, graph.stateVarName(), "", + statep->label()}; + declp->hier(scopep->prettyName()); + modp->addStmtsp(declp); + AstNodeExpr* const guardp + = andExpr(flp, + new AstNeq{flp, new AstVarRef{flp, prevVscp, VAccess::READ}, + makeStateConst(flp, prevVscp, statep->value())}, + new AstEq{flp, new AstVarRef{flp, stateVscp, VAccess::READ}, + makeStateConst(flp, stateVscp, statep->value())}); + covPostp->addStmtsp(new AstIf{flp, guardp, new AstCoverInc{flp, declp}}); + } + + for (const V3GraphVertex& vtx : graph.vertices()) { + const FsmVertex* const fromVertexp = vtx.as(); + for (const V3GraphEdge& edge : fromVertexp->outEdges()) { + const FsmArcEdge* const arcp = edge.as(); + const FsmStateVertex* const toStatep = arcp->top()->as(); + // Arc coverage mirrors the extracted graph exactly, including + // reset and synthetic-default sources, so reports match the + // reviewer-visible graph dump and the user-visible annotation. + const string resetTag + = arcp->isReset() ? (graph.resetInclude() ? "[reset_include]" : "[reset]") : ""; + const string fsmTag = arcp->isReset() ? (graph.resetInclude() ? "reset_include" + : "reset") + : arcp->isDefault() ? "default" + : ""; + AstCoverOtherDecl* const declp = new AstCoverOtherDecl{ + flp, "v_fsm_arc/" + modp->prettyName(), + graph.stateVarName() + "::" + fromVertexp->label() + "->" + toStatep->label() + + resetTag, + "", + 0, + graph.stateVarName(), + fromVertexp->label(), + toStatep->label(), + fsmTag}; + declp->hier(scopep->prettyName()); + modp->addStmtsp(declp); + AstNodeExpr* guardp = nullptr; + if (fromVertexp->isResetAny()) { + // Reset arcs are modeled as pseudo-source edges in the + // graph, then reconstructed here into the original simple + // reset predicate combined with the destination state. + guardp = buildResetCond(flp, graph.resetCond().varScopep, graph.resetCond()); + guardp = andExpr(flp, guardp, + new AstEq{flp, new AstVarRef{flp, stateVscp, VAccess::READ}, + makeStateConst(flp, stateVscp, toStatep->value())}); + } else if (fromVertexp->isDefaultAny()) { + // Synthetic default arcs mean "none of the explicit + // source states matched", so rebuild that as a conjunction + // of previous-state != known-state tests. + for (const V3GraphVertex& stateVtx : graph.vertices()) { + const FsmVertex* const stateVertexp = stateVtx.as(); + if (!stateVertexp->isState()) continue; + guardp = andExpr( + flp, guardp, + new AstNeq{flp, new AstVarRef{flp, prevVscp, VAccess::READ}, + makeStateConst(flp, prevVscp, stateVertexp->value())}); + } + guardp = andExpr(flp, guardp, + new AstEq{flp, new AstVarRef{flp, stateVscp, VAccess::READ}, + makeStateConst(flp, stateVscp, toStatep->value())}); + } else { + guardp = andExpr( + flp, + new AstEq{flp, new AstVarRef{flp, prevVscp, VAccess::READ}, + makeStateConst(flp, prevVscp, fromVertexp->value())}, + new AstEq{flp, new AstVarRef{flp, stateVscp, VAccess::READ}, + makeStateConst(flp, stateVscp, toStatep->value())}); + } + covPostp->addStmtsp(new AstIf{flp, guardp, new AstCoverInc{flp, declp}}); + } + } + + AstSenTree* const sentreep = buildSenTree(flp, graph.senses()); + AstActive* const activep = new AstActive{flp, "fsm-coverage", sentreep}; + activep->senTreeStorep(sentreep); + scopep->addBlocksp(activep); + activep->addStmtsp(covPostp); + } + +public: + // CONSTRUCTORS + // Lower every detected FSM graph from the shared local state into + // concrete coverage instrumentation while the saved scoped pointers are + // still valid in the same pass. + explicit FsmLowerVisitor(const FsmState& state) + : m_state{state} { + for (const std::pair& it : m_state.fsms()) { + buildOne(*it.second.graphp); + } + } +}; + +} // namespace + +void V3FsmDetect::detect(AstNetlist* rootp) { + UINFO(2, __FUNCTION__ << ":"); + FsmState state; + // Phase 1: recover each supported FSM into a complete graph while the + // original clocked/case structure is still easy to recognize. + FsmDetectVisitor detect{state, rootp}; + if (dumpGraphLevel() >= 6) { + size_t index = 0; + for (const std::pair& it : state.fsms()) { + it.second.graphp->dumpDotFilePrefixed(it.second.graphp->dumpTag(index++)); + } + } + // Phase 2: lower the completed in-memory graph state immediately, without + // crossing into another pass owner or serializing through AST placeholders. + { FsmLowerVisitor lower{state}; } + V3Global::dumpCheckGlobalTree("fsm-detect", 0, dumpTreeEitherLevel() >= 3); +} diff --git a/src/V3FsmDetect.h b/src/V3FsmDetect.h new file mode 100644 index 000000000..878efe5be --- /dev/null +++ b/src/V3FsmDetect.h @@ -0,0 +1,33 @@ +// -*- mode: C++; c-file-style: "cc-mode" -*- +//************************************************************************* +// DESCRIPTION: Verilator: FSM coverage detect pass +// +// Code available from: https://verilator.org +// +//************************************************************************* +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of either the GNU Lesser General Public License Version 3 +// or the Perl Artistic License Version 2.0. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 +// +//************************************************************************* + +#ifndef VERILATOR_V3FSMDETECT_H_ +#define VERILATOR_V3FSMDETECT_H_ + +#include "config_build.h" +#include "verilatedos.h" + +class AstNetlist; + +class V3FsmDetect final { +public: + // Detect FSMs while the original clocked/case structure is still visible, + // then immediately lower the recovered graphs into concrete coverage + // instrumentation as a second local phase in the same pass. + static void detect(AstNetlist* rootp) VL_MT_DISABLED; +}; + +#endif diff --git a/src/V3LinkParse.cpp b/src/V3LinkParse.cpp index 1014e6332..f75526eac 100644 --- a/src/V3LinkParse.cpp +++ b/src/V3LinkParse.cpp @@ -611,6 +611,18 @@ class LinkParseVisitor final : public VNVisitor { m_varp->attrSplitVar(true); } VL_DO_DANGLING(nodep->unlinkFrBack()->deleteTree(), nodep); + } else if (nodep->attrType() == VAttrType::VAR_FSM_ARC_INCLUDE_COND) { + UASSERT_OBJ(m_varp, nodep, "Attribute not attached to variable"); + m_varp->attrFsmArcInclCond(true); + VL_DO_DANGLING(nodep->unlinkFrBack()->deleteTree(), nodep); + } else if (nodep->attrType() == VAttrType::VAR_FSM_RESET_ARC) { + UASSERT_OBJ(m_varp, nodep, "Attribute not attached to variable"); + m_varp->attrFsmResetArc(true); + VL_DO_DANGLING(nodep->unlinkFrBack()->deleteTree(), nodep); + } else if (nodep->attrType() == VAttrType::VAR_FSM_STATE) { + UASSERT_OBJ(m_varp, nodep, "Attribute not attached to variable"); + m_varp->attrFsmState(true); + VL_DO_DANGLING(nodep->unlinkFrBack()->deleteTree(), nodep); } else if (nodep->attrType() == VAttrType::VAR_SC_BIGUINT) { UASSERT_OBJ(m_varp, nodep, "Attribute not attached to variable"); m_varp->attrScBigUint(true); diff --git a/src/V3Options.cpp b/src/V3Options.cpp index b5943701b..6736434fb 100644 --- a/src/V3Options.cpp +++ b/src/V3Options.cpp @@ -1352,6 +1352,7 @@ void V3Options::parseOptsList(FileLine* fl, const string& optdir, int argc, DECL_OPTION("-coverage", CbOnOff, [this](bool flag) { coverage(flag); }); DECL_OPTION("-coverage-expr", OnOff, &m_coverageExpr); DECL_OPTION("-coverage-expr-max", Set, &m_coverageExprMax); + DECL_OPTION("-coverage-fsm", OnOff, &m_coverageFsm); DECL_OPTION("-coverage-line", OnOff, &m_coverageLine); DECL_OPTION("-coverage-max-width", Set, &m_coverageMaxWidth); DECL_OPTION("-coverage-toggle", OnOff, &m_coverageToggle); diff --git a/src/V3Options.h b/src/V3Options.h index 868cd45a8..3bc930724 100644 --- a/src/V3Options.h +++ b/src/V3Options.h @@ -226,6 +226,7 @@ private: bool m_build = false; // main switch: --build bool m_context = true; // main switch: --Wcontext bool m_coverageExpr = false; // main switch: --coverage-expr + bool m_coverageFsm = false; // main switch: --coverage-fsm bool m_coverageLine = false; // main switch: --coverage-block bool m_coverageToggle = false; // main switch: --coverage-toggle bool m_coverageUnderscore = false; // main switch: --coverage-underscore @@ -447,7 +448,8 @@ private: void optimize(int level); void showVersion(bool verbose); void coverage(bool flag) { - m_coverageLine = m_coverageToggle = m_coverageExpr = m_coverageUser = flag; + m_coverageLine = m_coverageToggle = m_coverageExpr = m_coverageFsm = m_coverageUser + = flag; } static bool suffixed(const string& sw, const char* arg); static string parseFileArg(const string& optdir, const string& relfilename); @@ -508,9 +510,19 @@ public: void buildDepBin(const string& flag) { m_buildDepBin = flag; } bool context() const VL_MT_SAFE { return m_context; } bool coverage() const VL_MT_SAFE { + // Any enabled coverage kind, including FSM coverage. Code generation + // and runtime support should generally query this accessor. + return m_coverageLine || m_coverageToggle || m_coverageExpr || m_coverageUser + || m_coverageFsm; + } + bool coverageNonFsm() const VL_MT_SAFE { + // The broad line/toggle/expr/user coverage transforms use this + // accessor. FSM coverage shares the overall coverage umbrella, but its + // extraction still happens through a separate early-recognition path. return m_coverageLine || m_coverageToggle || m_coverageExpr || m_coverageUser; } bool coverageExpr() const { return m_coverageExpr; } + bool coverageFsm() const { return m_coverageFsm; } bool coverageLine() const { return m_coverageLine; } bool coverageToggle() const { return m_coverageToggle; } bool coverageUnderscore() const { return m_coverageUnderscore; } diff --git a/src/Verilator.cpp b/src/Verilator.cpp index 3154d537a..e1ac5e054 100644 --- a/src/Verilator.cpp +++ b/src/Verilator.cpp @@ -54,6 +54,7 @@ #include "V3Expand.h" #include "V3File.h" #include "V3Force.h" +#include "V3FsmDetect.h" #include "V3Fork.h" #include "V3FuncOpt.h" #include "V3Gate.h" @@ -232,9 +233,11 @@ static void process() { v3Global.vlExit(0); } - // Coverage insertion - // Before we do dead code elimination and inlining, or we'll lose it. - if (v3Global.opt.coverage()) V3Coverage::coverage(v3Global.rootp()); + // Insert generic non-FSM coverage before dead code elimination and + // inlining, or those opportunities may be optimized away. FSM + // coverage is handled later in V3FsmDetect, after scoping has created + // the AST context needed to recover and lower FSMs reliably. + if (v3Global.opt.coverageNonFsm()) V3Coverage::coverage(v3Global.rootp()); // Resolve randsequence if they are used by the design if (v3Global.useRandSequence()) V3RandSequence::randSequenceNetlist(v3Global.rootp()); @@ -347,6 +350,12 @@ static void process() { // No more AstAlias after linkDotScope V3Scope::scopeAll(v3Global.rootp()); V3LinkDot::linkDotScope(v3Global.rootp()); + // FSM coverage needs scopes, but should otherwise run as early as + // possible before later lowering rewrites user-visible clocked + // case structure. This entry point runs two adjacent phases: + // detect into local graph state, then lower that completed state + // into the concrete coverage machinery. + if (v3Global.opt.coverageFsm()) V3FsmDetect::detect(v3Global.rootp()); // Relocate classes (after linkDot) V3Class::classAll(v3Global.rootp()); @@ -428,8 +437,9 @@ static void process() { "This may cause ordering problems."); } - // Combine COVERINCs with duplicate terms - if (v3Global.opt.coverage()) V3CoverageJoin::coverageJoin(v3Global.rootp()); + // Combine generic COVERINCs with duplicate terms. FSM coverage is + // already lowered separately inside V3FsmDetect. + if (v3Global.opt.coverageNonFsm()) V3CoverageJoin::coverageJoin(v3Global.rootp()); // Remove unused vars V3Const::constifyAll(v3Global.rootp()); @@ -447,7 +457,7 @@ static void process() { } // Create delayed assignments - // This creates lots of duplicate ACTIVES so ActiveTop needs to be after this step + // This creates lots of duplicate ACTIVES so ActiveTop needs to be after this step. V3Delayed::delayedAll(v3Global.rootp()); // Make Active's on the top level. diff --git a/src/VlcMain.cpp b/src/VlcMain.cpp index 2226db37e..fb485c500 100644 --- a/src/VlcMain.cpp +++ b/src/VlcMain.cpp @@ -69,6 +69,7 @@ void VlcOptions::parseOptsList(int argc, char** argv) { DECL_OPTION("-debug", CbCall, []() { V3Error::debugDefault(3); }); DECL_OPTION("-debugi", CbVal, [](int v) { V3Error::debugDefault(v); }); DECL_OPTION("-filter-type", Set, &m_filterType); + DECL_OPTION("-include-reset-arcs", OnOff, &m_includeResetArcs); DECL_OPTION("-rank", OnOff, &m_rank); DECL_OPTION("-unlink", OnOff, &m_unlink); DECL_OPTION("-V", CbCall, []() { diff --git a/src/VlcOptions.h b/src/VlcOptions.h index 7d957f722..3e46db033 100644 --- a/src/VlcOptions.h +++ b/src/VlcOptions.h @@ -39,6 +39,7 @@ class VlcOptions final { bool m_annotateAll = false; // main switch: --annotate-all int m_annotateMin = 10; // main switch: --annotate-min I bool m_annotatePoints = false; // main switch: --annotate-points + bool m_includeResetArcs = false; // main switch: --include-reset-arcs string m_filterType = "*"; // main switch: --filter-type VlStringSet m_readFiles; // main switch: --read bool m_rank = false; // main switch: --rank @@ -67,6 +68,7 @@ public: int annotateMin() const { return m_annotateMin; } bool countOk(uint64_t count) const { return count >= static_cast(m_annotateMin); } bool annotatePoints() const { return m_annotatePoints; } + bool includeResetArcs() const { return m_includeResetArcs; } bool rank() const { return m_rank; } bool unlink() const { return m_unlink; } string writeFile() const { return m_writeFile; } diff --git a/src/VlcPoint.h b/src/VlcPoint.h index 372b9f2bf..94fba6028 100644 --- a/src/VlcPoint.h +++ b/src/VlcPoint.h @@ -70,6 +70,18 @@ public: return keyExtract(VL_CIK_THRESH, m_name.c_str()); } string linescov() const { return keyExtract(VL_CIK_LINESCOV, m_name.c_str()); } + bool isFsmState() const { return type() == "fsm_state"; } + bool isFsmArc() const { return type() == "fsm_arc"; } + // Arc-specific helpers are used after callers have already filtered to + // FSM arc points, so they do not repeat the type check here. + string fsmVarName() const { return keyExtract(VL_CIK_FSM_VAR, m_name.c_str()); } + string fsmFromState() const { return keyExtract(VL_CIK_FSM_FROM, m_name.c_str()); } + string fsmToState() const { return keyExtract(VL_CIK_FSM_TO, m_name.c_str()); } + string fsmTag() const { return keyExtract(VL_CIK_FSM_TAG, m_name.c_str()); } + bool isFsmResetInclude() const { return fsmTag() == "reset_include"; } + bool isFsmResetArc() const { return fsmTag() == "reset"; } + bool isFsmDefaultArc() const { return fsmTag() == "default"; } + bool fsmIsReset() const { return isFsmResetArc() || isFsmResetInclude(); } int lineno() const { const string lineStr = keyExtract(VL_CIK_LINENO, m_name.c_str()); return std::atoi(lineStr.c_str()); diff --git a/src/VlcTop.cpp b/src/VlcTop.cpp index 5c0876f16..37cee819d 100644 --- a/src/VlcTop.cpp +++ b/src/VlcTop.cpp @@ -25,6 +25,8 @@ #include #include +#include +#include #include #include @@ -122,12 +124,18 @@ void VlcTop::writeInfo(const string& filename) { int branchesHit = 0; for (auto& li : lines) { VlcSourceCount& sc = li.second; - os << "DA:" << sc.lineno() << "," << sc.maxCount() << "\n"; - const int num_branches = sc.points().size(); - if (num_branches == 1) continue; - branchesFound += num_branches; - int point_num = 0; + uint64_t daCount = 0; + std::vector infoPoints; for (const auto& point : sc.points()) { + if (point->isFsmArc()) continue; + daCount = std::max(daCount, point->count()); + if (!point->isFsmState()) infoPoints.push_back(point); + } + os << "DA:" << sc.lineno() << "," << daCount << "\n"; + if (infoPoints.size() <= 1) continue; + branchesFound += static_cast(infoPoints.size()); + int point_num = 0; + for (const VlcPoint* point : infoPoints) { os << "BRDA:" << sc.lineno() << ","; os << "0,"; os << point_num << ","; @@ -328,6 +336,29 @@ void VlcTop::annotateOutputFiles(const string& dirname) { if (opt.annotatePoints()) { for (const auto& pit : sc.points()) pit->dumpAnnotate(os, opt.annotateMin()); } + bool printedFsmHeader = false; + for (const auto& pit : sc.points()) { + if (!pit->isFsmState() && !pit->isFsmArc()) continue; + if (!printedFsmHeader) { + os << " // [FSM coverage]\n"; + printedFsmHeader = true; + } + os << (opt.countOk(pit->count()) ? " " : "%"); + os << std::setfill('0') << std::setw(6) << pit->count() << " "; + if (pit->isFsmState()) { + os << "// [fsm_state " << pit->comment() << "]"; + if (pit->count() == 0) os << " *** UNCOVERED ***"; + os << "\n"; + } else if (pit->isFsmDefaultArc()) { + os << "// [SYNTHETIC DEFAULT ARC: " << pit->comment() << "]\n"; + } else { + os << "// [fsm_arc " << pit->comment() << "]"; + if (pit->fsmIsReset() && !opt.includeResetArcs()) { + os << " [reset arc, excluded from %]"; + } + os << "\n"; + } + } } } } diff --git a/src/verilog.l b/src/verilog.l index 4e4796a5c..4dc79f210 100644 --- a/src/verilog.l +++ b/src/verilog.l @@ -848,6 +848,14 @@ vnum {vnum1}|{vnum2}|{vnum3}|{vnum4}|{vnum5} "/*verilator sc_clock*/" { FL; yylval.fl->v3warn(DEPRECATED, "sc_clock is ignored"); FL_BRK; } "/*verilator sformat*/" { FL; return yVL_SFORMAT; } "/*verilator split_var*/" { FL; return yVL_SPLIT_VAR; } + /* Experimental Verilator-specific FSM coverage controls. These names were + * chosen to match the current extractor behavior, not a published synthesis + * or simulator pragma standard, so they may evolve as we settle on longer- + * term compatibility/aliasing. + */ + "/*verilator fsm_arc_include_cond*/" { FL; return yVL_FSM_ARC_INCL_COND; } + "/*verilator fsm_reset_arc*/" { FL; return yVL_FSM_RESET_ARC; } + "/*verilator fsm_state*/" { FL; return yVL_FSM_STATE; } "/*verilator tag"[^*]*"*/" { FL; yylval.strp = PARSEP->newString(V3ParseImp::lexParseTag(yytext)); return yVL_TAG; } "/*verilator timing_off*/" { FL_FWD; PARSEP->lexFileline()->timingOn(false); FL_BRK; } diff --git a/src/verilog.y b/src/verilog.y index b6c513ddb..0f689647d 100644 --- a/src/verilog.y +++ b/src/verilog.y @@ -804,6 +804,9 @@ BISONPRE_VERSION(3.7,%define api.header.include {"V3ParseBison.h"}) %token yVL_SC_BV "/*verilator sc_bv*/" %token yVL_SFORMAT "/*verilator sformat*/" %token yVL_SPLIT_VAR "/*verilator split_var*/" +%token yVL_FSM_ARC_INCL_COND "/*verilator fsm_arc_include_cond*/" +%token yVL_FSM_RESET_ARC "/*verilator fsm_reset_arc*/" +%token yVL_FSM_STATE "/*verilator fsm_state*/" %token yVL_TAG "/*verilator tag*/" %token yVL_UNROLL_DISABLE "/*verilator unroll_disable*/" %token yVL_UNROLL_FULL "/*verilator unroll_full*/" @@ -3123,6 +3126,9 @@ sigAttr: | yVL_SC_BV { $$ = new AstAttrOf{$1, VAttrType::VAR_SC_BV}; } | yVL_SFORMAT { $$ = new AstAttrOf{$1, VAttrType::VAR_SFORMAT}; } | yVL_SPLIT_VAR { $$ = new AstAttrOf{$1, VAttrType::VAR_SPLIT_VAR}; } + | yVL_FSM_ARC_INCL_COND { $$ = new AstAttrOf{$1, VAttrType::VAR_FSM_ARC_INCLUDE_COND}; } + | yVL_FSM_RESET_ARC { $$ = new AstAttrOf{$1, VAttrType::VAR_FSM_RESET_ARC}; } + | yVL_FSM_STATE { $$ = new AstAttrOf{$1, VAttrType::VAR_FSM_STATE}; } ; rangeListE: // IEEE: [{packed_dimension}] diff --git a/test_regress/t/t_cover_fsm_basic.out b/test_regress/t/t_cover_fsm_basic.out new file mode 100644 index 000000000..8fc6b1c5c --- /dev/null +++ b/test_regress/t/t_cover_fsm_basic.out @@ -0,0 +1,65 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM coverage basic test + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + module t ( + input clk + ); + + typedef enum logic [1:0] { + S_IDLE = 2'd0, + S_RUN = 2'd1, + S_DONE = 2'd2, + S_ERR = 2'd3 + } state_t; + + logic rst; + logic start; + integer cyc; + state_t state /*verilator fsm_reset_arc*/; + + initial begin + rst = 1'b1; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + always_ff @(posedge clk) begin + if (rst) begin + state <= S_IDLE; + end else begin +%000004 case (state) + // [FSM coverage] +%000001 // [fsm_arc t.state::ANY->S_IDLE[reset_include]] [reset arc, excluded from %] +%000004 // [fsm_arc t.state::S_DONE->S_DONE] +%000003 // [fsm_arc t.state::S_IDLE->S_IDLE] +%000001 // [fsm_arc t.state::S_IDLE->S_RUN] +%000001 // [fsm_arc t.state::S_RUN->S_DONE] +%000001 // [fsm_state t.state::S_DONE] +%000000 // [fsm_state t.state::S_ERR] *** UNCOVERED *** +%000000 // [fsm_state t.state::S_IDLE] *** UNCOVERED *** +%000001 // [fsm_state t.state::S_RUN] + S_IDLE: if (start) state <= S_RUN; else state <= S_IDLE; + S_RUN: state <= S_DONE; + S_DONE: state <= S_DONE; + default: state <= S_ERR; + endcase + end + end + + endmodule + diff --git a/test_regress/t/t_cover_fsm_basic.py b/test_regress/t/t_cover_fsm_basic.py new file mode 100644 index 000000000..628914b4f --- /dev/null +++ b/test_regress/t/t_cover_fsm_basic.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage basic test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import os + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--cc --coverage-fsm']) + +test.execute() + +test.run(cmd=[ + os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--annotate", + test.obj_dir + "/annotated", + test.obj_dir + "/coverage.dat", +], + verilator_run=True) + +test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_cover_fsm_basic.v b/test_regress/t/t_cover_fsm_basic.v new file mode 100644 index 000000000..237d89e84 --- /dev/null +++ b/test_regress/t/t_cover_fsm_basic.v @@ -0,0 +1,53 @@ +// DESCRIPTION: Verilator: FSM coverage basic test +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input clk +); + + typedef enum logic [1:0] { + S_IDLE = 2'd0, + S_RUN = 2'd1, + S_DONE = 2'd2, + S_ERR = 2'd3 + } state_t; + + logic rst; + logic start; + integer cyc; + state_t state /*verilator fsm_reset_arc*/; + + initial begin + rst = 1'b1; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + always_ff @(posedge clk) begin + if (rst) begin + state <= S_IDLE; + end else begin + case (state) + S_IDLE: if (start) state <= S_RUN; else state <= S_IDLE; + S_RUN: state <= S_DONE; + S_DONE: state <= S_DONE; + default: state <= S_ERR; + endcase + end + end + +endmodule diff --git a/test_regress/t/t_cover_fsm_beginif.out b/test_regress/t/t_cover_fsm_beginif.out new file mode 100644 index 000000000..d24de8d04 --- /dev/null +++ b/test_regress/t/t_cover_fsm_beginif.out @@ -0,0 +1,63 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM coverage begin-wrapped/if-else test + // + // This program is free software; you can redistribute it and/or modify it + // under the terms of either the GNU Lesser General Public License Version 3 + // or the Perl Artistic License Version 2.0. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + + module t( + input logic clk + ); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + logic rst; + logic sel; + int cyc; + state_t state /*verilator fsm_reset_arc*/; + + initial begin + rst = 1'b1; + sel = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) sel <= 1'b1; + if (cyc == 3) sel <= 1'b0; + if (cyc == 6) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + always_ff @(posedge clk) begin + if (rst) begin + state <= S0; + end else begin +%000003 case (state) + // [FSM coverage] +%000001 // [fsm_arc t.state::ANY->S0[reset_include]] [reset arc, excluded from %] +%000000 // [fsm_arc t.state::S0->S1] +%000003 // [fsm_arc t.state::S0->S2] +%000000 // [fsm_arc t.state::S1->S0] +%000002 // [fsm_state t.state::S0] +%000000 // [fsm_state t.state::S1] *** UNCOVERED *** +%000003 // [fsm_state t.state::S2] + S0: if (sel) state <= S1; else state <= S2; + S1: state <= S0; + default: state <= S0; + endcase + end + end + + endmodule + diff --git a/test_regress/t/t_cover_fsm_beginif.py b/test_regress/t/t_cover_fsm_beginif.py new file mode 100644 index 000000000..46a0e0d22 --- /dev/null +++ b/test_regress/t/t_cover_fsm_beginif.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage begin-wrapped/if-else extraction test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import os + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--cc --coverage-fsm']) +test.execute() + +# Use annotated-source output so the expected file captures both the extracted +# FSM shape and the per-point hit counts. +test.run(cmd=[ + os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--annotate", + test.obj_dir + "/annotated", + test.obj_dir + "/coverage.dat", +], + verilator_run=True) + +test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_cover_fsm_beginif.v b/test_regress/t/t_cover_fsm_beginif.v new file mode 100644 index 000000000..54a569f92 --- /dev/null +++ b/test_regress/t/t_cover_fsm_beginif.v @@ -0,0 +1,53 @@ +// DESCRIPTION: Verilator: FSM coverage begin-wrapped/if-else test +// +// This program is free software; you can redistribute it and/or modify it +// under the terms of either the GNU Lesser General Public License Version 3 +// or the Perl Artistic License Version 2.0. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +module t( + input logic clk +); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + logic rst; + logic sel; + int cyc; + state_t state /*verilator fsm_reset_arc*/; + + initial begin + rst = 1'b1; + sel = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) sel <= 1'b1; + if (cyc == 3) sel <= 1'b0; + if (cyc == 6) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + always_ff @(posedge clk) begin + if (rst) begin + state <= S0; + end else begin + case (state) + S0: if (sel) state <= S1; else state <= S2; + S1: state <= S0; + default: state <= S0; + endcase + end + end + +endmodule diff --git a/test_regress/t/t_cover_fsm_decldump.py b/test_regress/t/t_cover_fsm_decldump.py new file mode 100644 index 000000000..663e9ec29 --- /dev/null +++ b/test_regress/t/t_cover_fsm_decldump.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM lowered coverage declaration dump test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +from pathlib import Path + +import vltest_bootstrap + +test.scenarios('vlt') +test.top_filename = "t/t_cover_fsm_styles.v" + +# Dump the lowered AST so AstCoverOtherDecl::dump() sees FSM metadata-bearing +# coverage declarations directly. This avoids JSON/schema coupling while still +# covering the dump-side formatting for fv/ff/ft/fg. +test.lint(v_flags=["--coverage-fsm", "--dump-tree"]) + +tree_files = [Path(filename) for filename in test.glob_some(test.obj_dir + "/*.tree")] +tree_texts = [filename.read_text(encoding="utf8") for filename in tree_files] + +assert any("COVEROTHERDECL" in text and " fv=t.state" in text for text in tree_texts) +assert any( + "COVEROTHERDECL" in text and " ff=ANY" in text and " ft=S0" in text and " fg=reset" in text + for text in tree_texts +) +assert any( + "COVEROTHERDECL" in text and " ff=default" in text and " ft=S0" in text and " fg=default" + in text + for text in tree_texts +) + +test.passes() diff --git a/test_regress/t/t_cover_fsm_enum_bad.out b/test_regress/t/t_cover_fsm_enum_bad.out new file mode 100644 index 000000000..5dc28a897 --- /dev/null +++ b/test_regress/t/t_cover_fsm_enum_bad.out @@ -0,0 +1,6 @@ +%Warning-COVERIGN: t/t_cover_fsm_enum_bad.v:27:19: Ignoring unsupported: FSM coverage on enum state transitions that assign a constant not present in the declared enum + 27 | S0: state <= 2'd3; + | ^~ + ... For warning description see https://verilator.org/warn/COVERIGN?v=latest + ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. +%Error: Exiting due to diff --git a/test_regress/t/t_cover_fsm_enum_bad.py b/test_regress/t/t_cover_fsm_enum_bad.py new file mode 100644 index 000000000..b08aae26f --- /dev/null +++ b/test_regress/t/t_cover_fsm_enum_bad.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM enum transition bad-value test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +# When an enum-backed FSM assigns a constant that is not one of the declared +# enum items, FSM coverage should warn and skip the unsupported edge rather +# than turning optional coverage into a hard compile failure. +test.lint( + verilator_flags2=["--coverage-fsm"], + fails=True) + +test.file_grep( + test.compile_log_filename, + r'%Warning-COVERIGN: t/t_cover_fsm_enum_bad.v:27:19: Ignoring unsupported: FSM coverage ' + r'on enum state transitions that assign a constant not present in the declared enum') + +test.passes() diff --git a/test_regress/t/t_cover_fsm_enum_bad.v b/test_regress/t/t_cover_fsm_enum_bad.v new file mode 100644 index 000000000..a33717471 --- /dev/null +++ b/test_regress/t/t_cover_fsm_enum_bad.v @@ -0,0 +1,34 @@ +// DESCRIPTION: Verilator: FSM enum transition rejects unknown constant values +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input logic clk, + input logic rst +); + + typedef enum logic [1:0] { + S0, S1 + } state_t; + + state_t state; + + // FSM coverage should reject a constant next-state value that is not one of + // the declared enum items. This keeps graph construction aligned with the + // enum-backed state set instead of silently dropping the transition. + always_ff @(posedge clk) begin + if (rst) begin + state <= S0; + end else begin + case (state) + /* verilator lint_off ENUMVALUE */ + S0: state <= 2'd3; + /* verilator lint_on ENUMVALUE */ + default: state <= S0; + endcase + end + end + +endmodule diff --git a/test_regress/t/t_cover_fsm_enumwide_bad.out b/test_regress/t/t_cover_fsm_enumwide_bad.out new file mode 100644 index 000000000..62da6b270 --- /dev/null +++ b/test_regress/t/t_cover_fsm_enumwide_bad.out @@ -0,0 +1,6 @@ +%Warning-COVERIGN: t/t_cover_fsm_enumwide_bad.v:25:7: Ignoring unsupported: FSM coverage on enum-typed state variables wider than 32 bits + 25 | case (state) + | ^~~~ + ... For warning description see https://verilator.org/warn/COVERIGN?v=latest + ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. +%Error: Exiting due to diff --git a/test_regress/t/t_cover_fsm_enumwide_bad.py b/test_regress/t/t_cover_fsm_enumwide_bad.py new file mode 100644 index 000000000..b0745b266 --- /dev/null +++ b/test_regress/t/t_cover_fsm_enumwide_bad.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM enum width limit test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +# FSM coverage currently stores recovered enum state values in the detector's +# 32-bit internal representation, so wider enum-backed FSMs are rejected. +test.lint( + verilator_flags2=["--coverage-fsm"], + fails=True) + +test.file_grep( + test.compile_log_filename, + r'%Warning-COVERIGN: t/t_cover_fsm_enumwide_bad.v:25:7: Ignoring unsupported: ' + r'FSM coverage on enum-typed state variables wider than 32 bits') + +test.passes() diff --git a/test_regress/t/t_cover_fsm_enumwide_bad.v b/test_regress/t/t_cover_fsm_enumwide_bad.v new file mode 100644 index 000000000..0687f5369 --- /dev/null +++ b/test_regress/t/t_cover_fsm_enumwide_bad.v @@ -0,0 +1,32 @@ +// DESCRIPTION: Verilator: FSM enum width limit rejects >32-bit enums +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input logic clk, + input logic rst +); + + typedef enum logic [32:0] { + S0 = 33'd0, + S1 = 33'd1 + } state_t; + + state_t state; + + // FSM coverage currently supports enum-backed state variables only up to + // 32 bits wide, so this wider enum should be rejected at FSM detection time. + always_ff @(posedge clk) begin + if (rst) begin + state <= S0; + end else begin + case (state) + S0: state <= S1; + default: state <= S0; + endcase + end + end + +endmodule diff --git a/test_regress/t/t_cover_fsm_flag_off.py b/test_regress/t/t_cover_fsm_flag_off.py new file mode 100644 index 000000000..e3fdca440 --- /dev/null +++ b/test_regress/t/t_cover_fsm_flag_off.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage stays off without --coverage-fsm +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--cc --coverage-line']) + +test.execute() + +test.file_grep_not(test.obj_dir + "/coverage.dat", r"fsm_state") +test.file_grep_not(test.obj_dir + "/coverage.dat", r"fsm_arc") + +test.passes() diff --git a/test_regress/t/t_cover_fsm_flag_off.v b/test_regress/t/t_cover_fsm_flag_off.v new file mode 100644 index 000000000..4005f8888 --- /dev/null +++ b/test_regress/t/t_cover_fsm_flag_off.v @@ -0,0 +1,53 @@ +// DESCRIPTION: Verilator: FSM coverage stays off without --coverage-fsm +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input clk +); + + typedef enum logic [1:0] { + S_IDLE = 2'd0, + S_RUN = 2'd1, + S_DONE = 2'd2, + S_ERR = 2'd3 + } state_t; + + logic rst; + logic start; + integer cyc; + state_t state /*verilator fsm_reset_arc*/; + + initial begin + rst = 1'b1; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + always_ff @(posedge clk) begin + if (rst) begin + state <= S_IDLE; + end else begin + case (state) + S_IDLE: if (start) state <= S_RUN; else state <= S_IDLE; + S_RUN: state <= S_DONE; + S_DONE: state <= S_DONE; + default: state <= S_ERR; + endcase + end + end + +endmodule diff --git a/test_regress/t/t_cover_fsm_forced.out b/test_regress/t/t_cover_fsm_forced.out new file mode 100644 index 000000000..d111c0069 --- /dev/null +++ b/test_regress/t/t_cover_fsm_forced.out @@ -0,0 +1,51 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM coverage forced non-enum test + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + module t ( + input clk + ); + + integer cyc; + logic rst; + logic [1:0] state /*verilator fsm_state*/; + + initial begin + cyc = 0; + rst = 1'b1; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 6) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + always_ff @(posedge clk) begin + if (rst) begin + state <= 2'd0; + end else begin +%000002 case (state) + // [FSM coverage] +%000001 // [fsm_arc t.state::ANY->S0[reset]] [reset arc, excluded from %] +%000002 // [fsm_arc t.state::S0->S1] +%000002 // [fsm_arc t.state::S1->S2] +%000001 // [fsm_state t.state::S0] +%000002 // [fsm_state t.state::S1] +%000002 // [fsm_state t.state::S2] +%000000 // [fsm_state t.state::S3] *** UNCOVERED *** + 2'd0: state <= 2'd1; + 2'd1: state <= 2'd2; + default: state <= 2'd0; + endcase + end + end + + endmodule + diff --git a/test_regress/t/t_cover_fsm_forced.py b/test_regress/t/t_cover_fsm_forced.py new file mode 100644 index 000000000..f368d9cf9 --- /dev/null +++ b/test_regress/t/t_cover_fsm_forced.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage forced non-enum test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import os + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--cc --coverage-fsm']) + +test.execute() + +# Use annotated-source golden output so hit-count regressions are visible in the +# expected file instead of being hidden behind coarse coverage.dat greps. +test.run(cmd=[ + os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--annotate", + test.obj_dir + "/annotated", + test.obj_dir + "/coverage.dat", +], + verilator_run=True) + +test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_cover_fsm_forced.v b/test_regress/t/t_cover_fsm_forced.v new file mode 100644 index 000000000..c10812859 --- /dev/null +++ b/test_regress/t/t_cover_fsm_forced.v @@ -0,0 +1,41 @@ +// DESCRIPTION: Verilator: FSM coverage forced non-enum test +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input clk +); + + integer cyc; + logic rst; + logic [1:0] state /*verilator fsm_state*/; + + initial begin + cyc = 0; + rst = 1'b1; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 6) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + always_ff @(posedge clk) begin + if (rst) begin + state <= 2'd0; + end else begin + case (state) + 2'd0: state <= 2'd1; + 2'd1: state <= 2'd2; + default: state <= 2'd0; + endcase + end + end + +endmodule diff --git a/test_regress/t/t_cover_fsm_graphdump.py b/test_regress/t/t_cover_fsm_graphdump.py new file mode 100644 index 000000000..3290be13f --- /dev/null +++ b/test_regress/t/t_cover_fsm_graphdump.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage graph dump test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vltmt') +test.top_filename = "t/t_cover_fsm_styles.v" + +test.compile(v_flags2=["--coverage-fsm", "--dumpi-graph", "6"], threads=2) + +dot_files = test.glob_some(test.obj_dir + "/*fsm_*.dot") +for dot_filename in dot_files: + test.file_grep(dot_filename, r'digraph v3graph') + +test.file_grep_any(dot_files, r'ANY') +test.file_grep_any(dot_files, r'default') + +test.passes() diff --git a/test_regress/t/t_cover_fsm_negative_extract.out b/test_regress/t/t_cover_fsm_negative_extract.out new file mode 100644 index 000000000..88ac5402d --- /dev/null +++ b/test_regress/t/t_cover_fsm_negative_extract.out @@ -0,0 +1,64 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM coverage negative extraction test + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + module t ( + input logic clk + ); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + int cyc; + logic side; + state_t state /*verilator fsm_reset_arc*/; + + initial begin + cyc = 0; + side = 1'b0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) side <= 1'b1; + if (cyc == 5) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + // The S0 arm is the supported baseline. The S1 and default arms are + // deliberately unsupported extractor shapes: one has two meaningful + // statements, the other writes a different lhs first. Coverage should ignore + // those arcs rather than guessing. + always_ff @(posedge clk) begin + if (cyc == 0) begin + state <= S0; + end else begin +%000002 case (state) + // [FSM coverage] +%000002 // [fsm_arc t.state::S0->S1] +%000001 // [fsm_state t.state::S0] +%000002 // [fsm_state t.state::S1] +%000002 // [fsm_state t.state::S2] + S0: state <= S1; + S1: begin + side <= ~side; + state <= S2; + end + default: begin + side <= 1'b0; + state <= S0; + end + endcase + end + end + + endmodule + diff --git a/test_regress/t/t_cover_fsm_negative_extract.py b/test_regress/t/t_cover_fsm_negative_extract.py new file mode 100644 index 000000000..53a99158c --- /dev/null +++ b/test_regress/t/t_cover_fsm_negative_extract.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage negative extraction test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import os + +import vltest_bootstrap + +test.scenarios('simulator') + +# This test is intentionally "half supported": one case item is a simple +# direct state assignment, while the others use shapes the extractor should +# ignore (multiple meaningful statements or assignment to a non-state lhs). +# That lets us hit the conservative negative branches in directStateAssign() +# and singleMeaningfulStmt() without changing user-visible behavior. +test.compile(verilator_flags2=['--cc --coverage-fsm']) + +test.execute() + +# Use annotated-source output so the golden locks down which candidate arcs +# survive extraction and which unsupported shapes are intentionally skipped. +test.run(cmd=[ + os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--annotate", + test.obj_dir + "/annotated", + test.obj_dir + "/coverage.dat", +], + verilator_run=True) + +test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_cover_fsm_negative_extract.v b/test_regress/t/t_cover_fsm_negative_extract.v new file mode 100644 index 000000000..1c60febde --- /dev/null +++ b/test_regress/t/t_cover_fsm_negative_extract.v @@ -0,0 +1,57 @@ +// DESCRIPTION: Verilator: FSM coverage negative extraction test +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input logic clk +); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + int cyc; + logic side; + state_t state /*verilator fsm_reset_arc*/; + + initial begin + cyc = 0; + side = 1'b0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) side <= 1'b1; + if (cyc == 5) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + // The S0 arm is the supported baseline. The S1 and default arms are + // deliberately unsupported extractor shapes: one has two meaningful + // statements, the other writes a different lhs first. Coverage should ignore + // those arcs rather than guessing. + always_ff @(posedge clk) begin + if (cyc == 0) begin + state <= S0; + end else begin + case (state) + S0: state <= S1; + S1: begin + side <= ~side; + state <= S2; + end + default: begin + side <= 1'b0; + state <= S0; + end + endcase + end + end + +endmodule diff --git a/test_regress/t/t_cover_fsm_noreset.out b/test_regress/t/t_cover_fsm_noreset.out new file mode 100644 index 000000000..f2c28be61 --- /dev/null +++ b/test_regress/t/t_cover_fsm_noreset.out @@ -0,0 +1,47 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM coverage no-reset lowering test + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + module t ( + input logic clk + ); + + typedef enum logic [0:0] { + S0 = 1'b0, + S1 = 1'b1 + } state_t; + + int cyc; + state_t state; + + initial begin + cyc = 0; + state = S0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 4) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + // No reset branch on purpose: this keeps the test focused on the branch in + // lowering that skips reset reconstruction entirely. + always_ff @(posedge clk) begin +%000003 case (state) + // [FSM coverage] +%000003 // [fsm_arc t.state::S0->S1] +%000002 // [fsm_state t.state::S0] +%000003 // [fsm_state t.state::S1] + S0: state <= S1; + default: state <= S0; + endcase + end + + endmodule + diff --git a/test_regress/t/t_cover_fsm_noreset.py b/test_regress/t/t_cover_fsm_noreset.py new file mode 100644 index 000000000..2f70ddb14 --- /dev/null +++ b/test_regress/t/t_cover_fsm_noreset.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage no-reset lowering test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import os + +import vltest_bootstrap + +test.scenarios('simulator') + +# This test deliberately uses a clocked FSM with no outer reset branch. It +# keeps coverage extraction in the supported subset, but forces lowering down +# the "hasResetCond() == false" path so we validate the no-reset machinery +# rather than only reset-wrapped FSMs. +test.compile(verilator_flags2=['--cc --coverage-fsm']) + +test.execute() + +# Use annotated-source output so the expected file captures the no-reset shape +# directly, including the absence of reset pseudo-arcs. +test.run(cmd=[ + os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--annotate", + test.obj_dir + "/annotated", + test.obj_dir + "/coverage.dat", +], + verilator_run=True) + +test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_cover_fsm_noreset.v b/test_regress/t/t_cover_fsm_noreset.v new file mode 100644 index 000000000..02d8fc1b7 --- /dev/null +++ b/test_regress/t/t_cover_fsm_noreset.v @@ -0,0 +1,41 @@ +// DESCRIPTION: Verilator: FSM coverage no-reset lowering test +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input logic clk +); + + typedef enum logic [0:0] { + S0 = 1'b0, + S1 = 1'b1 + } state_t; + + int cyc; + state_t state; + + initial begin + cyc = 0; + state = S0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 4) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + // No reset branch on purpose: this keeps the test focused on the branch in + // lowering that skips reset reconstruction entirely. + always_ff @(posedge clk) begin + case (state) + S0: state <= S1; + default: state <= S0; + endcase + end + +endmodule diff --git a/test_regress/t/t_cover_fsm_reset.out b/test_regress/t/t_cover_fsm_reset.out new file mode 100644 index 000000000..bda581f6a --- /dev/null +++ b/test_regress/t/t_cover_fsm_reset.out @@ -0,0 +1,63 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM coverage reset policy test + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + module t ( +%000006 input clk + ); + + typedef enum logic [0:0] { + S0 = 1'b0, + S1 = 1'b1 + } state_t; + +%000001 logic rst; + integer cyc; +%000001 state_t state_incl /*verilator fsm_reset_arc*/; +%000001 state_t state_excl; + +%000001 initial begin +%000001 rst = 1'b1; +%000001 cyc = 0; + end + +%000006 always @(posedge clk) begin +%000006 cyc <= cyc + 1; +%000005 if (cyc == 1) rst <= 1'b0; +%000005 if (cyc == 5) begin +%000001 $write("*-* All Finished *-*\n"); +%000001 $finish; + end + end + +%000006 always_ff @(posedge clk) begin +%000004 if (rst) state_incl <= S0; +%000004 else case (state_incl) + // [FSM coverage] +%000001 // [fsm_arc t.state_incl::ANY->S0[reset_include]] [reset arc, excluded from %] +%000001 // [fsm_arc t.state_incl::S0->S1] +%000000 // [fsm_state t.state_incl::S0] *** UNCOVERED *** +%000001 // [fsm_state t.state_incl::S1] +%000001 S0: state_incl <= S1; +%000003 default: state_incl <= S1; + endcase + end + +%000006 always_ff @(posedge clk) begin +%000004 if (rst) state_excl <= S0; +%000004 else case (state_excl) + // [FSM coverage] +%000001 // [fsm_arc t.state_excl::ANY->S0[reset]] [reset arc, excluded from %] +%000001 // [fsm_arc t.state_excl::S0->S1] +%000000 // [fsm_state t.state_excl::S0] *** UNCOVERED *** +%000001 // [fsm_state t.state_excl::S1] +%000001 S0: state_excl <= S1; +%000003 default: state_excl <= S1; + endcase + end + + endmodule + diff --git a/test_regress/t/t_cover_fsm_reset.py b/test_regress/t/t_cover_fsm_reset.py new file mode 100644 index 000000000..c250cbe45 --- /dev/null +++ b/test_regress/t/t_cover_fsm_reset.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage reset policy test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import os + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--cc --coverage']) + +test.execute() + +test.run(cmd=[ + os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--include-reset-arcs", + test.obj_dir + "/coverage.dat", +], + verilator_run=True) + +test.run(cmd=[ + os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--annotate", + test.obj_dir + "/annotated", + test.obj_dir + "/coverage.dat", +], + verilator_run=True) + +test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_cover_fsm_reset.v b/test_regress/t/t_cover_fsm_reset.v new file mode 100644 index 000000000..1855ef072 --- /dev/null +++ b/test_regress/t/t_cover_fsm_reset.v @@ -0,0 +1,51 @@ +// DESCRIPTION: Verilator: FSM coverage reset policy test +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input clk +); + + typedef enum logic [0:0] { + S0 = 1'b0, + S1 = 1'b1 + } state_t; + + logic rst; + integer cyc; + state_t state_incl /*verilator fsm_reset_arc*/; + state_t state_excl; + + initial begin + rst = 1'b1; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 5) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + always_ff @(posedge clk) begin + if (rst) state_incl <= S0; + else case (state_incl) + S0: state_incl <= S1; + default: state_incl <= S1; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_excl <= S0; + else case (state_excl) + S0: state_excl <= S1; + default: state_excl <= S1; + endcase + end + +endmodule diff --git a/test_regress/t/t_cover_fsm_reset_multi.out b/test_regress/t/t_cover_fsm_reset_multi.out new file mode 100644 index 000000000..bc22dfd47 --- /dev/null +++ b/test_regress/t/t_cover_fsm_reset_multi.out @@ -0,0 +1,62 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM coverage reset pseudo-vertex reuse test + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + module t ( + input clk + ); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + logic rst; + integer cyc; + state_t state /*verilator fsm_reset_arc*/; + + initial begin + rst = 1'b1; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 5) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + // This reset block is intentionally non-idiomatic. The detector only collects + // reset arcs from top-level direct assignments in the reset branch, so two + // sequential assignments are the narrowest way to force multiple reset arcs + // into one FSM graph and exercise reuse of the synthetic ANY reset source. + always_ff @(posedge clk) begin + if (rst) begin + state <= S0; + state <= S1; + end else begin +%000001 case (state) + // [FSM coverage] +%000000 // [fsm_arc t.state::ANY->S0[reset_include]] [reset arc, excluded from %] +%000001 // [fsm_arc t.state::ANY->S1[reset_include]] [reset arc, excluded from %] +%000000 // [fsm_arc t.state::S0->S2] +%000001 // [fsm_arc t.state::S1->S2] +%000000 // [fsm_state t.state::S0] *** UNCOVERED *** +%000001 // [fsm_state t.state::S1] +%000001 // [fsm_state t.state::S2] + S0: state <= S2; + S1: state <= S2; + default: state <= S2; + endcase + end + end + + endmodule + diff --git a/test_regress/t/t_cover_fsm_reset_multi.py b/test_regress/t/t_cover_fsm_reset_multi.py new file mode 100644 index 000000000..42ab4d0f3 --- /dev/null +++ b/test_regress/t/t_cover_fsm_reset_multi.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage reset pseudo-vertex reuse test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import os + +import vltest_bootstrap + +test.scenarios('simulator') + +# This regression is aimed at the graph helper, not at recommending RTL style. +# We deliberately create two reset arcs in a single FSM so graph construction +# has to reuse the synthetic ANY reset pseudo-vertex rather than allocating it +# only once for a one-arc machine. +test.compile(verilator_flags2=['--cc --coverage-fsm']) + +test.execute() + +# Use annotated-source output so the golden proves both reset arcs remain +# visible and share the same synthetic ANY reset source. +test.run(cmd=[ + os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--annotate", + test.obj_dir + "/annotated", + test.obj_dir + "/coverage.dat", +], + verilator_run=True) + +test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_cover_fsm_reset_multi.v b/test_regress/t/t_cover_fsm_reset_multi.v new file mode 100644 index 000000000..16b6de31f --- /dev/null +++ b/test_regress/t/t_cover_fsm_reset_multi.v @@ -0,0 +1,52 @@ +// DESCRIPTION: Verilator: FSM coverage reset pseudo-vertex reuse test +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input clk +); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + logic rst; + integer cyc; + state_t state /*verilator fsm_reset_arc*/; + + initial begin + rst = 1'b1; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 5) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + // This reset block is intentionally non-idiomatic. The detector only collects + // reset arcs from top-level direct assignments in the reset branch, so two + // sequential assignments are the narrowest way to force multiple reset arcs + // into one FSM graph and exercise reuse of the synthetic ANY reset source. + always_ff @(posedge clk) begin + if (rst) begin + state <= S0; + state <= S1; + end else begin + case (state) + S0: state <= S2; + S1: state <= S2; + default: state <= S2; + endcase + end + end + +endmodule diff --git a/test_regress/t/t_cover_fsm_styles.out b/test_regress/t/t_cover_fsm_styles.out new file mode 100644 index 000000000..33eba2621 --- /dev/null +++ b/test_regress/t/t_cover_fsm_styles.out @@ -0,0 +1,64 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM coverage style coverage test + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + module t ( + input clk + ); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2, + S3 = 2'd3 + } state_t; + + integer cyc; + logic rst; + logic start; + state_t state /*verilator fsm_arc_include_cond*/; + + initial begin + rst = 1'b1; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 6) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + always_ff @(posedge clk) begin + if (rst) begin + state <= S0; + end else begin +%000003 case (state) + // [FSM coverage] +%000001 // [fsm_arc t.state::ANY->S0[reset]] [reset arc, excluded from %] +%000000 // [fsm_arc t.state::S0->S1] +%000003 // [fsm_arc t.state::S0->S2] +%000000 // [fsm_arc t.state::S1->S3] +%000000 // [SYNTHETIC DEFAULT ARC: t.state::default->S0] +%000002 // [fsm_state t.state::S0] +%000000 // [fsm_state t.state::S1] *** UNCOVERED *** +%000003 // [fsm_state t.state::S2] +%000000 // [fsm_state t.state::S3] *** UNCOVERED *** + S0: if (start) state <= S1; else state <= S2; + S1: state <= S3; + default: state <= S0; + endcase + end + end + + endmodule + diff --git a/test_regress/t/t_cover_fsm_styles.py b/test_regress/t/t_cover_fsm_styles.py new file mode 100644 index 000000000..fc9fde9f0 --- /dev/null +++ b/test_regress/t/t_cover_fsm_styles.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage style coverage test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import os + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--cc --coverage-fsm']) + +test.execute() + +test.run(cmd=[os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--annotate", test.obj_dir + "/annotated", + test.obj_dir + "/coverage.dat"], + verilator_run=True) # yapf:disable + +test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_cover_fsm_styles.v b/test_regress/t/t_cover_fsm_styles.v new file mode 100644 index 000000000..e8300a9a9 --- /dev/null +++ b/test_regress/t/t_cover_fsm_styles.v @@ -0,0 +1,52 @@ +// DESCRIPTION: Verilator: FSM coverage style coverage test +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input clk +); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2, + S3 = 2'd3 + } state_t; + + integer cyc; + logic rst; + logic start; + state_t state /*verilator fsm_arc_include_cond*/; + + initial begin + rst = 1'b1; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 6) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + always_ff @(posedge clk) begin + if (rst) begin + state <= S0; + end else begin + case (state) + S0: if (start) state <= S1; else state <= S2; + S1: state <= S3; + default: state <= S0; + endcase + end + end + +endmodule diff --git a/test_regress/t/t_cover_otherdecl_dump.py b/test_regress/t/t_cover_otherdecl_dump.py new file mode 100644 index 000000000..4b8bbda4f --- /dev/null +++ b/test_regress/t/t_cover_otherdecl_dump.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: generic coverage declaration dump test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +from pathlib import Path + +import vltest_bootstrap + +test.scenarios('vlt') +test.top_filename = "t/t_cover_fsm_styles.v" + +# Dump generic COVEROTHERDECL nodes so AstCoverOtherDecl::dump() also sees +# coverage declarations with no FSM metadata, exercising the empty-field side +# of the fv/ff/ft/fg formatting. +test.lint(v_flags=["--coverage-line", "--dump-tree"]) + +tree_files = [Path(filename) for filename in test.glob_some(test.obj_dir + "/*.tree")] +tree_texts = [filename.read_text(encoding="utf8") for filename in tree_files] + +generic_lines = [] +for text in tree_texts: + generic_lines.extend( + line for line in text.splitlines() if "COVEROTHERDECL" in line and " page=v_line/" in line + ) + +assert generic_lines +assert any(" fv=" not in line and " ff=" not in line and " ft=" not in line and " fg=" not in line + for line in generic_lines) + +test.passes() diff --git a/test_regress/t/t_fsm_metacmt_dump.py b/test_regress/t/t_fsm_metacmt_dump.py new file mode 100644 index 000000000..d5a9dfe04 --- /dev/null +++ b/test_regress/t/t_fsm_metacmt_dump.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM metacomment dump test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import json + +import vltest_bootstrap + +test.scenarios('vlt') + +test.top_filename = "t/t_fsm_metacmt_dump.v" + +test.lint(v_flags=["--dump-tree --dump-tree-json --no-json-edit-nums"]) + +tree_files = test.glob_some(test.obj_dir + "/*.tree") +json_files = test.glob_some(test.obj_dir + "/*.tree.json") + +test.file_grep_any(tree_files, r'\[aFSMSTATE\]') +test.file_grep_any(tree_files, r'\[aFSMRESETARC\]') +test.file_grep_any(tree_files, r'\[aFSMARCCOND\]') + +test.file_grep_any(json_files, r'"attrFsmState":true') +test.file_grep_any(json_files, r'"attrFsmResetArc":true') +test.file_grep_any(json_files, r'"attrFsmArcInclCond":true') + +for filename in json_files: + with open(filename, 'r', encoding="utf8") as fh: + json.load(fh) + +test.passes() diff --git a/test_regress/t/t_fsm_metacmt_dump.v b/test_regress/t/t_fsm_metacmt_dump.v new file mode 100644 index 000000000..6206125e1 --- /dev/null +++ b/test_regress/t/t_fsm_metacmt_dump.v @@ -0,0 +1,34 @@ +// DESCRIPTION: Verilator: FSM metacomment dump test +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input logic clk, + input logic rst +); + + typedef enum logic [0:0] { + S0 = 1'b0, + S1 = 1'b1 + } state_t; + + state_t state_reset /*verilator fsm_reset_arc*/; + state_t state_cond /*verilator fsm_arc_include_cond*/; + logic forced_state /*verilator fsm_state*/; + + always_ff @(posedge clk) begin + if (rst) begin + state_reset <= S0; + state_cond <= S0; + forced_state <= 1'b0; + end else begin + state_reset <= S1; + if (state_cond) state_cond <= S0; + else state_cond <= S1; + forced_state <= ~forced_state; + end + end + +endmodule diff --git a/test_regress/t/t_fsmmulti_same_bad.out b/test_regress/t/t_fsmmulti_same_bad.out new file mode 100644 index 000000000..4ffe2d1c5 --- /dev/null +++ b/test_regress/t/t_fsmmulti_same_bad.out @@ -0,0 +1,6 @@ +%Warning-COVERIGN: t/t_fsmmulti_same_bad.v:30:5: Ignoring unsupported: FSM coverage on multiple supported case statements found in the same always block. Only the first candidate will be instrumented. + 30 | case (state) + | ^~~~ + ... For warning description see https://verilator.org/warn/COVERIGN?v=latest + ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. +%Error: Exiting due to diff --git a/test_regress/t/t_fsmmulti_same_bad.py b/test_regress/t/t_fsmmulti_same_bad.py new file mode 100644 index 000000000..3d36db93c --- /dev/null +++ b/test_regress/t/t_fsmmulti_same_bad.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: same-state multi-candidate FSM error test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +# Multiple supported case candidates on the same state variable in one +# always_ff now warn and keep only the first candidate instrumented. Different- +# state multi-candidate cases still use the existing FSMMULTI warning path; this +# test locks down only the same-state unsupported form. +test.lint( + verilator_flags2=["--coverage-fsm"], + fails=True) + +test.file_grep( + test.compile_log_filename, + r'%Warning-COVERIGN: t/t_fsmmulti_same_bad.v:30:5: Ignoring unsupported: FSM coverage on ' + r'multiple supported case statements found in the same always block. Only the first ' + r'candidate will be instrumented.') + +test.passes() diff --git a/test_regress/t/t_fsmmulti_same_bad.v b/test_regress/t/t_fsmmulti_same_bad.v new file mode 100644 index 000000000..ef99d5812 --- /dev/null +++ b/test_regress/t/t_fsmmulti_same_bad.v @@ -0,0 +1,36 @@ +// DESCRIPTION: Verilator: same-state multi-candidate FSM error test +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input logic clk, + input logic rst +); + + typedef enum logic [1:0] { + S0, S1, S2 + } state_t; + + state_t state; + + // This is intentionally non-idiomatic RTL. The detector sees one supported + // candidate in the reset-if else branch and a second supported top-level + // case on the same state variable. That same-state duplicate is rejected. + always_ff @(posedge clk) begin + if (rst) begin + state <= S0; + end else begin + case (state) + S0: state <= S1; + default: ; + endcase + end + case (state) + S1: state <= S2; + default: ; + endcase + end + +endmodule diff --git a/test_regress/t/t_fsmmulti_warn_bad.out b/test_regress/t/t_fsmmulti_warn_bad.out new file mode 100644 index 000000000..00c98c6ad --- /dev/null +++ b/test_regress/t/t_fsmmulti_warn_bad.out @@ -0,0 +1,6 @@ +%Warning-FSMMULTI: t/t_fsmmulti_warn_bad.v:27:5: FSM coverage: multiple enum-typed case statements found in the same always block. Only the first candidate will be instrumented. + 27 | case (state_b) + | ^~~~ + ... For warning description see https://verilator.org/warn/FSMMULTI?v=latest + ... Use "/* verilator lint_off FSMMULTI */" and lint_on around source to disable this message. +%Error: Exiting due to diff --git a/test_regress/t/t_fsmmulti_warn_bad.py b/test_regress/t/t_fsmmulti_warn_bad.py new file mode 100644 index 000000000..098562570 --- /dev/null +++ b/test_regress/t/t_fsmmulti_warn_bad.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSMMULTI warning test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.lint( + verilator_flags2=["--coverage-fsm"], + fails=True, + expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_fsmmulti_warn_bad.v b/test_regress/t/t_fsmmulti_warn_bad.v new file mode 100644 index 000000000..1c385d142 --- /dev/null +++ b/test_regress/t/t_fsmmulti_warn_bad.v @@ -0,0 +1,33 @@ +// DESCRIPTION: Verilator: FSMMULTI warning test +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input logic clk +); + + typedef enum logic [1:0] { + A0, A1 + } a_state_t; + + typedef enum logic [1:0] { + B0, B1 + } b_state_t; + + a_state_t state_a; + b_state_t state_b; + + always_ff @(posedge clk) begin + case (state_a) + A0: state_a <= A1; + default: state_a <= A0; + endcase + case (state_b) + B0: state_b <= B1; + default: state_b <= B0; + endcase + end + +endmodule diff --git a/test_regress/t/t_fsmmulti_warn_off.py b/test_regress/t/t_fsmmulti_warn_off.py new file mode 100644 index 000000000..018b61822 --- /dev/null +++ b/test_regress/t/t_fsmmulti_warn_off.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSMMULTI warning disabled without --coverage-fsm +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.lint(verilator_flags2=["--coverage-line"]) +test.file_grep_not(test.compile_log_filename, r"FSMMULTI") + +test.passes() diff --git a/test_regress/t/t_fsmmulti_warn_off.v b/test_regress/t/t_fsmmulti_warn_off.v new file mode 100644 index 000000000..4cfb1242d --- /dev/null +++ b/test_regress/t/t_fsmmulti_warn_off.v @@ -0,0 +1,42 @@ +// DESCRIPTION: Verilator: FSMMULTI warning disabled without --coverage-fsm +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t; + typedef enum logic [1:0] { + A0 = 2'd0, + A1 = 2'd1, + A2 = 2'd2 + } state_a_t; + + typedef enum logic [1:0] { + B0 = 2'd0, + B1 = 2'd1, + B2 = 2'd2 + } state_b_t; + + logic clk; + logic rst; + state_a_t a_state; + state_b_t b_state; + + always_ff @(posedge clk) begin + if (rst) begin + a_state <= A0; + b_state <= B0; + end else begin + case (a_state) + A0: a_state <= A1; + A1: a_state <= A2; + default: a_state <= A0; + endcase + case (b_state) + B0: b_state <= B1; + B1: b_state <= B2; + default: b_state <= B0; + endcase + end + end +endmodule diff --git a/test_regress/t/t_vlcov_fsm_report.out b/test_regress/t/t_vlcov_fsm_report.out new file mode 100644 index 000000000..ab2a70ea6 --- /dev/null +++ b/test_regress/t/t_vlcov_fsm_report.out @@ -0,0 +1,102 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM reporting coverage test + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + module t ( +%000007 input clk + ); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2, + S3 = 2'd3 + } state_t; + + integer cyc; +%000001 logic rst; +%000001 logic start; +%000003 state_t state_default /*verilator fsm_arc_include_cond*/; +%000001 state_t state_reset_incl /*verilator fsm_reset_arc*/; +%000001 state_t state_reset_excl; + +%000001 initial begin +%000001 rst = 1'b1; +%000001 start = 1'b0; +%000001 cyc = 0; + end + +%000007 always @(posedge clk) begin +%000007 cyc <= cyc + 1; +%000006 if (cyc == 1) rst <= 1'b0; +%000006 if (cyc == 2) start <= 1'b1; +%000006 if (cyc == 3) start <= 1'b0; +%000006 if (cyc == 6) begin +%000001 $write("*-* All Finished *-*\n"); +%000001 $finish; + end + end + + // This FSM gives the reporting path both ordinary arcs and a synthetic + // default arc so annotate/write-info exercise FSM-arc filtering. +%000007 always_ff @(posedge clk) begin +%000005 if (rst) begin +%000002 state_default <= S0; +%000005 end else begin +%000005 case (state_default) + // [FSM coverage] +%000001 // [fsm_arc t.state_default::ANY->S0[reset]] [reset arc, excluded from %] +%000000 // [SYNTHETIC DEFAULT ARC: t.state_default::default->S0] +%000002 // [fsm_state t.state_default::S0] +%000000 // [fsm_state t.state_default::S1] *** UNCOVERED *** +%000003 // [fsm_state t.state_default::S2] +%000000 // [fsm_state t.state_default::S3] *** UNCOVERED *** +%000003 S0: if (start) state_default <= S1; else state_default <= S2; +%000002 default: state_default <= S0; + endcase + end + end + + // These two FSMs give reporting both reset-include and reset-exclude arcs so + // annotate can exercise the reset-arc filtering wording in both modes. +%000007 always_ff @(posedge clk) begin +%000005 if (rst) begin +%000002 state_reset_incl <= S0; +%000005 end else begin +%000005 case (state_reset_incl) + // [FSM coverage] +%000001 // [fsm_arc t.state_reset_incl::ANY->S0[reset_include]] [reset arc, excluded from %] +%000001 // [fsm_arc t.state_reset_incl::S0->S1] +%000000 // [fsm_state t.state_reset_incl::S0] *** UNCOVERED *** +%000001 // [fsm_state t.state_reset_incl::S1] +%000000 // [fsm_state t.state_reset_incl::S2] *** UNCOVERED *** +%000000 // [fsm_state t.state_reset_incl::S3] *** UNCOVERED *** +%000001 S0: state_reset_incl <= S1; +%000004 default: state_reset_incl <= S1; + endcase + end + end + +%000007 always_ff @(posedge clk) begin +%000005 if (rst) begin +%000002 state_reset_excl <= S0; +%000005 end else begin +%000005 case (state_reset_excl) + // [FSM coverage] +%000001 // [fsm_arc t.state_reset_excl::ANY->S0[reset]] [reset arc, excluded from %] +%000001 // [fsm_arc t.state_reset_excl::S0->S1] +%000000 // [fsm_state t.state_reset_excl::S0] *** UNCOVERED *** +%000001 // [fsm_state t.state_reset_excl::S1] +%000000 // [fsm_state t.state_reset_excl::S2] *** UNCOVERED *** +%000000 // [fsm_state t.state_reset_excl::S3] *** UNCOVERED *** +%000001 S0: state_reset_excl <= S1; +%000004 default: state_reset_excl <= S1; + endcase + end + end + + endmodule + diff --git a/test_regress/t/t_vlcov_fsm_report.py b/test_regress/t/t_vlcov_fsm_report.py new file mode 100644 index 000000000..ccfc706ca --- /dev/null +++ b/test_regress/t/t_vlcov_fsm_report.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM reporting coverage test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import os + +import vltest_bootstrap + +test.scenarios('simulator') + +# This regression targets the reporting side of FSM coverage rather than the +# detector itself. The generated coverage.dat contains state points, ordinary +# arcs, default arcs, reset arcs, and reset-include arcs so verilator_coverage +# exercises the FSM-specific filtering and annotation code paths. +test.compile(verilator_flags2=['--cc --coverage']) + +test.execute() + +test.run(cmd=[ + os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--write-info", + test.obj_dir + "/coverage.info", + test.obj_dir + "/coverage.dat", +], + verilator_run=True) + +test.file_grep(test.obj_dir + "/coverage.info", r"TN:verilator_coverage") +test.file_grep(test.obj_dir + "/coverage.info", r"BRF:") +test.file_grep(test.obj_dir + "/coverage.info", r"BRH:") + +test.run(cmd=[os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--annotate", test.obj_dir + "/annotated", + test.obj_dir + "/coverage.dat"], + verilator_run=True) # yapf:disable + +test.run(cmd=[os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--include-reset-arcs", + "--annotate", test.obj_dir + "/annotated-incl", + test.obj_dir + "/coverage.dat"], + verilator_run=True) # yapf:disable + +annotated = test.obj_dir + "/annotated/t_vlcov_fsm_report.v" +annotated_incl = test.obj_dir + "/annotated-incl/t_vlcov_fsm_report.v" + +test.files_identical(annotated, "t/t_vlcov_fsm_report.out") +test.files_identical(annotated_incl, "t/t_vlcov_fsm_report_incl.out") + +test.passes() diff --git a/test_regress/t/t_vlcov_fsm_report.v b/test_regress/t/t_vlcov_fsm_report.v new file mode 100644 index 000000000..a2c3498bb --- /dev/null +++ b/test_regress/t/t_vlcov_fsm_report.v @@ -0,0 +1,79 @@ +// DESCRIPTION: Verilator: FSM reporting coverage test +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input clk +); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2, + S3 = 2'd3 + } state_t; + + integer cyc; + logic rst; + logic start; + state_t state_default /*verilator fsm_arc_include_cond*/; + state_t state_reset_incl /*verilator fsm_reset_arc*/; + state_t state_reset_excl; + + initial begin + rst = 1'b1; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 6) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + // This FSM gives the reporting path both ordinary arcs and a synthetic + // default arc so annotate/write-info exercise FSM-arc filtering. + always_ff @(posedge clk) begin + if (rst) begin + state_default <= S0; + end else begin + case (state_default) + S0: if (start) state_default <= S1; else state_default <= S2; + default: state_default <= S0; + endcase + end + end + + // These two FSMs give reporting both reset-include and reset-exclude arcs so + // annotate can exercise the reset-arc filtering wording in both modes. + always_ff @(posedge clk) begin + if (rst) begin + state_reset_incl <= S0; + end else begin + case (state_reset_incl) + S0: state_reset_incl <= S1; + default: state_reset_incl <= S1; + endcase + end + end + + always_ff @(posedge clk) begin + if (rst) begin + state_reset_excl <= S0; + end else begin + case (state_reset_excl) + S0: state_reset_excl <= S1; + default: state_reset_excl <= S1; + endcase + end + end + +endmodule diff --git a/test_regress/t/t_vlcov_fsm_report_incl.out b/test_regress/t/t_vlcov_fsm_report_incl.out new file mode 100644 index 000000000..a9a1c89d8 --- /dev/null +++ b/test_regress/t/t_vlcov_fsm_report_incl.out @@ -0,0 +1,102 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM reporting coverage test + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + module t ( +%000007 input clk + ); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2, + S3 = 2'd3 + } state_t; + + integer cyc; +%000001 logic rst; +%000001 logic start; +%000003 state_t state_default /*verilator fsm_arc_include_cond*/; +%000001 state_t state_reset_incl /*verilator fsm_reset_arc*/; +%000001 state_t state_reset_excl; + +%000001 initial begin +%000001 rst = 1'b1; +%000001 start = 1'b0; +%000001 cyc = 0; + end + +%000007 always @(posedge clk) begin +%000007 cyc <= cyc + 1; +%000006 if (cyc == 1) rst <= 1'b0; +%000006 if (cyc == 2) start <= 1'b1; +%000006 if (cyc == 3) start <= 1'b0; +%000006 if (cyc == 6) begin +%000001 $write("*-* All Finished *-*\n"); +%000001 $finish; + end + end + + // This FSM gives the reporting path both ordinary arcs and a synthetic + // default arc so annotate/write-info exercise FSM-arc filtering. +%000007 always_ff @(posedge clk) begin +%000005 if (rst) begin +%000002 state_default <= S0; +%000005 end else begin +%000005 case (state_default) + // [FSM coverage] +%000001 // [fsm_arc t.state_default::ANY->S0[reset]] +%000000 // [SYNTHETIC DEFAULT ARC: t.state_default::default->S0] +%000002 // [fsm_state t.state_default::S0] +%000000 // [fsm_state t.state_default::S1] *** UNCOVERED *** +%000003 // [fsm_state t.state_default::S2] +%000000 // [fsm_state t.state_default::S3] *** UNCOVERED *** +%000003 S0: if (start) state_default <= S1; else state_default <= S2; +%000002 default: state_default <= S0; + endcase + end + end + + // These two FSMs give reporting both reset-include and reset-exclude arcs so + // annotate can exercise the reset-arc filtering wording in both modes. +%000007 always_ff @(posedge clk) begin +%000005 if (rst) begin +%000002 state_reset_incl <= S0; +%000005 end else begin +%000005 case (state_reset_incl) + // [FSM coverage] +%000001 // [fsm_arc t.state_reset_incl::ANY->S0[reset_include]] +%000001 // [fsm_arc t.state_reset_incl::S0->S1] +%000000 // [fsm_state t.state_reset_incl::S0] *** UNCOVERED *** +%000001 // [fsm_state t.state_reset_incl::S1] +%000000 // [fsm_state t.state_reset_incl::S2] *** UNCOVERED *** +%000000 // [fsm_state t.state_reset_incl::S3] *** UNCOVERED *** +%000001 S0: state_reset_incl <= S1; +%000004 default: state_reset_incl <= S1; + endcase + end + end + +%000007 always_ff @(posedge clk) begin +%000005 if (rst) begin +%000002 state_reset_excl <= S0; +%000005 end else begin +%000005 case (state_reset_excl) + // [FSM coverage] +%000001 // [fsm_arc t.state_reset_excl::ANY->S0[reset]] +%000001 // [fsm_arc t.state_reset_excl::S0->S1] +%000000 // [fsm_state t.state_reset_excl::S0] *** UNCOVERED *** +%000001 // [fsm_state t.state_reset_excl::S1] +%000000 // [fsm_state t.state_reset_excl::S2] *** UNCOVERED *** +%000000 // [fsm_state t.state_reset_excl::S3] *** UNCOVERED *** +%000001 S0: state_reset_excl <= S1; +%000004 default: state_reset_excl <= S1; + endcase + end + end + + endmodule +