From 74ae91a6d0c208059f62f9f8cf9bdde5bc0075d8 Mon Sep 17 00:00:00 2001 From: Wilson Snyder Date: Sun, 26 Apr 2026 01:58:27 -0400 Subject: [PATCH 001/350] devel release --- CMakeLists.txt | 2 +- Changes | 6 ++++++ configure.ac | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4eea5ca22..d16d14736 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,7 +16,7 @@ cmake_minimum_required(VERSION 3.15) cmake_policy(SET CMP0091 NEW) # Use MSVC_RUNTIME_LIBRARY to select the runtime project( Verilator - VERSION 5.048 + VERSION 5.049 HOMEPAGE_URL https://verilator.org LANGUAGES CXX ) diff --git a/Changes b/Changes index 5403c5b2d..c24ba6440 100644 --- a/Changes +++ b/Changes @@ -8,6 +8,12 @@ The changes in each Verilator version are described below. The contributors that suggested or implemented a given issue are shown in []. Thanks! +Verilator 5.049 devel +========================== + +* TBD + + Verilator 5.048 2026-04-26 ========================== diff --git a/configure.ac b/configure.ac index 719efd92b..2cc95c267 100644 --- a/configure.ac +++ b/configure.ac @@ -12,7 +12,7 @@ # Then 'make maintainer-dist' #AC_INIT([Verilator],[#.### YYYY-MM-DD]) #AC_INIT([Verilator],[#.### devel]) -AC_INIT([Verilator],[5.048 2026-04-26], +AC_INIT([Verilator],[5.049 devel], [https://verilator.org], [verilator],[https://verilator.org]) From 2875ec8655a1a5000a95a9bf0fb5ec44e49fc0e1 Mon Sep 17 00:00:00 2001 From: Wilson Snyder Date: Sun, 26 Apr 2026 02:00:32 -0400 Subject: [PATCH 002/350] Commentary: Version release process --- docs/internals.rst | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/internals.rst b/docs/internals.rst index 237ce3030..881885ec2 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -2689,6 +2689,55 @@ xsim_flags / xsim_flags2 / xsim_run_flags only for use with the Xilinx XSim simulator. +Version Release Process +======================= + +This section documents the process to release a new version. This is not +intended to be run by other than the primary repository admins. + +#. If activity requires, announce release pending as GitHub issue. +#. Check all test suites pass: + + - rtlmeter (or wait until overnight cron passes). + - sv-tests (run manually or wait a few days and check proper version was + tested). + - uvm (currently requires private accellera access). + - verilator-ext-tests: ``make pull && make clean && make test`` + +#. Check documentation: + + - ``nodist/log_changes # and update Changes`` + - ``make spelling # and check warnings`` + - ``make docs # and check warnings`` + +#. Prepare release: + + - edit ``Changes`` to update version banner. + - edit ``configure.ac`` to change version number and "devel" to proper + ISO-format date. + - edit ``CMakeLists.txt`` version number. + +#. Test and release: + + - ``git commit -am "Version bump"`` + - ``make distclean && autoconf && ./configure && make test`` + - ``make maintainer-dist`` + +#. Wait for GitHub CI build actions to pass. +#. File announcement issue at verilator-announce, pasting in ``Changes`` + information. +#. Devel release: + + - edit ``Changes``, ``configure.ac``, ``CMakeLists.txt`` to devel and + devel version number. + - ``git commit -am "devel release"`` + - ``make distclean && autoconf && ./configure && make test`` + - ``git push`` + +#. Merge GitHub pull requests tagged ``status:merge-after-release``. +#. Check ``docs/guide/deprecations.rst`` for things due to remove. + + Distribution ============ From 982b9ee2198bc2b6800bf953ce14fcff41ebd8e6 Mon Sep 17 00:00:00 2001 From: hjsanjana Date: Sun, 26 Apr 2026 03:24:40 -0700 Subject: [PATCH 003/350] Fix std::randomize internal error on static member of different class (#7498) (#7499) Fixes #7498. --- docs/CONTRIBUTORS | 1 + src/V3Randomize.cpp | 5 ++ .../t/t_std_randomize_static_member.py | 21 +++++ .../t/t_std_randomize_static_member.v | 80 +++++++++++++++++++ 4 files changed, 107 insertions(+) create mode 100644 test_regress/t/t_std_randomize_static_member.py create mode 100644 test_regress/t/t_std_randomize_static_member.v diff --git a/docs/CONTRIBUTORS b/docs/CONTRIBUTORS index fc824f1c8..1dc86878e 100644 --- a/docs/CONTRIBUTORS +++ b/docs/CONTRIBUTORS @@ -88,6 +88,7 @@ Gökçe Aydos Han Qi Harald Heckmann Hennadii Chernyshchyk +hjsanjana Howard Su Huang Rui Huanghuang Zhou diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index b012a9c89..67da65c05 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -2824,6 +2824,11 @@ class CaptureVisitor final : public VNVisitor { && varRefp->varp()->lifetime().isStatic()) return CaptureMode::CAP_NO; if (callerIsClass && varIsFieldOfCaller) return CaptureMode::CAP_THIS; + // Static member of a different class: V3Width replaces AstMemberSel with a bare + // AstVarRef (classOrPackagep set). Capture current value as a function argument. + if (callerIsClass && varClassp && !varIsFieldOfCaller + && varRefp->varp()->lifetime().isStatic()) + return CaptureMode::CAP_VALUE; UASSERT_OBJ(!callerIsClass, varRefp, "Invalid reference?"); return CaptureMode::CAP_VALUE; } diff --git a/test_regress/t/t_std_randomize_static_member.py b/test_regress/t/t_std_randomize_static_member.py new file mode 100644 index 000000000..db1adb3f9 --- /dev/null +++ b/test_regress/t/t_std_randomize_static_member.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +if not test.have_solver: + test.skip("No constraint solver installed") + +test.compile() + +test.execute() + +test.passes() diff --git a/test_regress/t/t_std_randomize_static_member.v b/test_regress/t/t_std_randomize_static_member.v new file mode 100644 index 000000000..c80d3336a --- /dev/null +++ b/test_regress/t/t_std_randomize_static_member.v @@ -0,0 +1,80 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain +// SPDX-FileCopyrightText: 2026 Antmicro +// SPDX-License-Identifier: CC0-1.0 + +// Regression test for GitHub issue #7498: +// std::randomize() constraint referencing a static class member of a +// *different* class (accessed via a foreach loop) caused an internal error +// in V3Randomize ("Invalid reference?"). + +package tlogy_m_pkg; +class v_cfg; + static int num_of_ds = 0; +endclass + +class tlogy_m; + v_cfg v[$]; +endclass +endpackage + +package s_pkg; +import tlogy_m_pkg::*; +class s_cfg; + tlogy_m t_m; + bit t_mode; + int a_d_idx; + + function void setup_h_iw_cfg(); + if (t_mode) begin + foreach (t_m.v[i]) begin + if (std::randomize(a_d_idx) with { + if (t_m.v[i].num_of_ds > 1) { + a_d_idx inside {[0:(t_m.v[i].num_of_ds - 1)]}; + a_d_idx != 0; + } + } == 0) + $stop; + end + end + endfunction +endclass +endpackage + +module t; + import tlogy_m_pkg::*; + import s_pkg::*; + + initial begin + automatic s_cfg cfg = new; + automatic tlogy_m tm = new; + automatic v_cfg vc0 = new; + automatic v_cfg vc1 = new; + + // Set up: push two entries in the queue and set num_of_ds = 3 + // so the constraint branch (num_of_ds > 1) is exercised. + tm.v.push_back(vc0); + tm.v.push_back(vc1); + v_cfg::num_of_ds = 3; + + cfg.t_m = tm; + cfg.t_mode = 1'b1; + + repeat (20) begin + cfg.setup_h_iw_cfg(); + // When num_of_ds = 3 the constraint is: + // a_d_idx inside {[0:2]} && a_d_idx != 0 + // so a_d_idx must be 1 or 2. + if (cfg.a_d_idx < 1 || cfg.a_d_idx > 2) $stop; + end + + // Also verify that the "no constraint" branch (num_of_ds <= 1) + // compiles and runs without crashing. + v_cfg::num_of_ds = 0; + cfg.setup_h_iw_cfg(); + + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule From e4803f29b6ce998575a1adeef64e1ae0c617fef6 Mon Sep 17 00:00:00 2001 From: github action Date: Sun, 26 Apr 2026 10:29:56 +0000 Subject: [PATCH 004/350] Apply 'make format' --- test_regress/t/t_std_randomize_static_member.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 test_regress/t/t_std_randomize_static_member.py diff --git a/test_regress/t/t_std_randomize_static_member.py b/test_regress/t/t_std_randomize_static_member.py old mode 100644 new mode 100755 From c8893b64de2ee6dcef18283bceaf97096b6dbc5a Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Mon, 27 Apr 2026 14:20:34 +0200 Subject: [PATCH 005/350] Support `always` / `always[m:n]` / `s_always[m:n]` property operators (#7482) --- src/V3AssertNfa.cpp | 27 ++++++ src/V3AstNodeExpr.h | 26 ++++++ src/V3AstNodes.cpp | 8 ++ src/V3EmitV.cpp | 12 +++ src/V3Width.cpp | 59 ++++++++++++ src/verilog.y | 12 ++- test_regress/t/t_assert_always_unsup.out | 16 +--- test_regress/t/t_assert_always_unsup.v | 8 -- test_regress/t/t_debug_emitv.out | 16 ++++ test_regress/t/t_debug_emitv.v | 3 + test_regress/t/t_prop_always.py | 18 ++++ test_regress/t/t_prop_always.v | 107 ++++++++++++++++++++++ test_regress/t/t_prop_always_bad.out | 34 +++++++ test_regress/t/t_prop_always_bad.py | 16 ++++ test_regress/t/t_prop_always_bad.v | 20 ++++ test_regress/t/t_prop_always_unsup.out | 18 ++++ test_regress/t/t_prop_always_unsup.py | 16 ++++ test_regress/t/t_prop_always_unsup.v | 19 ++++ test_regress/t/t_property_pexpr_unsup.out | 12 --- test_regress/t/t_property_unsup.out | 66 +++++-------- test_regress/t/t_property_unsup.v | 6 -- 21 files changed, 435 insertions(+), 84 deletions(-) create mode 100755 test_regress/t/t_prop_always.py create mode 100644 test_regress/t/t_prop_always.v create mode 100644 test_regress/t/t_prop_always_bad.out create mode 100755 test_regress/t/t_prop_always_bad.py create mode 100644 test_regress/t/t_prop_always_bad.v create mode 100644 test_regress/t/t_prop_always_unsup.out create mode 100755 test_regress/t/t_prop_always_unsup.py create mode 100644 test_regress/t/t_prop_always_unsup.v diff --git a/src/V3AssertNfa.cpp b/src/V3AssertNfa.cpp index a2fd9234c..bb388b846 100644 --- a/src/V3AssertNfa.cpp +++ b/src/V3AssertNfa.cpp @@ -528,6 +528,30 @@ class SvaNfaBuilder final { return {currentp, nullptr, {}}; } + // always[lo:hi] / s_always[lo:hi] (IEEE 1800-2023 16.12.11). + BuildResult buildPropAlways(AstPropAlways* nodep, SvaStateVertex* entryVtxp, + bool isTopLevelStep = false) { + FileLine* const flp = nodep->fileline(); + AstNodeExpr* const propp = nodep->propp(); + const int lo = getConstInt(nodep->loBoundp()); + const int hi = getConstInt(nodep->hiBoundp()); + UASSERT_OBJ(lo >= 0 && hi >= lo, nodep, "PropAlways bounds invariant (V3Width)"); + SvaStateVertex* currentp = addDelayChain(entryVtxp, lo, flp); + for (int k = 0; k <= hi - lo; ++k) { + if (k > 0) { + SvaStateVertex* const nextp = scopedCreateVertex(); + guardedEdge(currentp, nextp, flp); + currentp = nextp; + } + SvaStateVertex* const checkp = scopedCreateVertex(); + SvaTransEdge* const linkp + = guardedLink(currentp, checkp, sampled(propp->cloneTreePure(false)), flp); + if (isTopLevelStep && !m_inUnboundedScope) linkp->m_rejectOnFail = true; + currentp = checkp; + } + return {currentp, nullptr, {}}; + } + BuildResult buildGotoRep(AstSGotoRep* repp, SvaStateVertex* entryVtxp) { FileLine* const flp = repp->fileline(); AstNodeExpr* const exprp = repp->exprp(); @@ -734,6 +758,9 @@ public: if (AstSConsRep* const repp = VN_CAST(nodep, SConsRep)) { return buildConsRep(repp, entryVtxp, isTopLevelStep); } + if (AstPropAlways* const alwaysp = VN_CAST(nodep, PropAlways)) { + return buildPropAlways(alwaysp, entryVtxp, isTopLevelStep); + } if (AstSGotoRep* const repp = VN_CAST(nodep, SGotoRep)) { return buildGotoRep(repp, entryVtxp); } diff --git a/src/V3AstNodeExpr.h b/src/V3AstNodeExpr.h index 2d156f3fb..56d302730 100644 --- a/src/V3AstNodeExpr.h +++ b/src/V3AstNodeExpr.h @@ -2027,6 +2027,32 @@ public: } string name() const override VL_MT_STABLE { return m_name; } }; +class AstPropAlways final : public AstNodeExpr { + // always[m:n] / s_always[m:n] (IEEE 1800-2023 16.12.11) + // @astgen op1 := propp : AstNodeExpr + // @astgen op2 := loBoundp : AstNodeExpr + // @astgen op3 := hiBoundp : AstNodeExpr + const bool m_isStrong = false; // s_always +public: + AstPropAlways(FileLine* fl, AstNodeExpr* propp, AstNodeExpr* loBoundp, AstNodeExpr* hiBoundp, + bool isStrong) + : ASTGEN_SUPER_PropAlways(fl) + , m_isStrong{isStrong} { + this->propp(propp); + this->loBoundp(loBoundp); + this->hiBoundp(hiBoundp); + } + ASTGEN_MEMBERS_AstPropAlways; + void dump(std::ostream& str) const override; + void dumpJson(std::ostream& str) const override; + string emitVerilog() override { V3ERROR_NA_RETURN(""); } + string emitC() override { V3ERROR_NA_RETURN(""); } + string emitSimpleOperator() override { V3ERROR_NA_RETURN(""); } + bool cleanOut() const override { V3ERROR_NA_RETURN(""); } + int instrCount() const override { V3ERROR_NA_RETURN(0); } + bool isStrong() const { return m_isStrong; } + bool isMultiCycleSva() const override { return true; } +}; class AstRand final : public AstNodeExpr { // $random/$random(seed) or $urandom/$urandom(seed) // Return a random number, based upon width() diff --git a/src/V3AstNodes.cpp b/src/V3AstNodes.cpp index 510703e4f..9745f65b8 100644 --- a/src/V3AstNodes.cpp +++ b/src/V3AstNodes.cpp @@ -456,6 +456,14 @@ void AstSConsRep::dumpJson(std::ostream& str) const { dumpJsonBoolFuncIf(str, unbounded); dumpJsonGen(str); } // LCOV_EXCL_STOP +void AstPropAlways::dump(std::ostream& str) const { + this->AstNodeExpr::dump(str); + if (isStrong()) str << " [strong]"; +} +void AstPropAlways::dumpJson(std::ostream& str) const { + dumpJsonBoolFuncIf(str, isStrong); + dumpJsonGen(str); +} void AstConsQueue::dump(std::ostream& str) const { this->AstNodeExpr::dump(str); if (lhsIsValue()) str << " [LVAL]"; diff --git a/src/V3EmitV.cpp b/src/V3EmitV.cpp index ea82d070c..a53be9ecd 100644 --- a/src/V3EmitV.cpp +++ b/src/V3EmitV.cpp @@ -1058,6 +1058,18 @@ class EmitVBaseVisitorConst VL_NOT_FINAL : public VNVisitorConst { puts("\n"); } void visit(AstPExpr* nodep) override { iterateConst(nodep->bodyp()); } + void visit(AstPropAlways* nodep) override { + puts(nodep->isStrong() ? "s_always" : "always"); + if (!VN_IS(nodep->loBoundp(), Unbounded) || !VN_IS(nodep->hiBoundp(), Unbounded)) { + puts(" ["); + iterateConst(nodep->loBoundp()); + puts(":"); + iterateConst(nodep->hiBoundp()); + puts("]"); + } + puts(" "); + iterateConst(nodep->propp()); + } void visit(AstSExpr* nodep) override { iterateConstNull(nodep->preExprp()); { diff --git a/src/V3Width.cpp b/src/V3Width.cpp index 34d60cf06..c0ce22de3 100644 --- a/src/V3Width.cpp +++ b/src/V3Width.cpp @@ -1545,6 +1545,65 @@ class WidthVisitor final : public VNVisitor { nodep->dtypeSetBit(); } } + void visit(AstPropAlways* nodep) override { + // IEEE 1800-2023 16.12.11 + assertAtExpr(nodep); + if (m_vup->prelim()) { + userIterateAndNext(nodep->propp(), WidthVP{SELF, BOTH}.p()); + if (!VN_IS(nodep->loBoundp(), Unbounded)) { + userIterateAndNext(nodep->loBoundp(), WidthVP{SELF, BOTH}.p()); + V3Const::constifyParamsEdit(nodep->loBoundp()); + } + if (!VN_IS(nodep->hiBoundp(), Unbounded)) { + userIterateAndNext(nodep->hiBoundp(), WidthVP{SELF, BOTH}.p()); + V3Const::constifyParamsEdit(nodep->hiBoundp()); + } + const bool loUnbounded = VN_IS(nodep->loBoundp(), Unbounded); + const bool hiUnbounded = VN_IS(nodep->hiBoundp(), Unbounded); + if (loUnbounded || hiUnbounded) { + if (nodep->isStrong()) { + nodep->v3error("s_always range must be bounded (IEEE 1800-2023 16.12.11)"); + } else { + nodep->v3warn(E_UNSUPPORTED, + "Unsupported: unbounded always range (always [m:$])"); + } + nodep->dtypeSetBit(); + return; + } + const AstConst* const loConstp = VN_CAST(nodep->loBoundp(), Const); + const AstConst* const hiConstp = VN_CAST(nodep->hiBoundp(), Const); + if (!loConstp) { + nodep->v3error("always range low bound must be a constant expression" + " (IEEE 1800-2023 16.12.11)"); + } + if (!hiConstp) { + nodep->v3error("always range high bound must be a constant expression" + " (IEEE 1800-2023 16.12.11)"); + } + if (loConstp && loConstp->toSInt() < 0) { + nodep->v3error("always range low bound must be non-negative" + " (IEEE 1800-2023 16.12.11)"); + } + if (loConstp && hiConstp && hiConstp->toSInt() < loConstp->toSInt()) { + nodep->v3error("always range high bound must be >= low bound" + " (IEEE 1800-2023 16.12.11)"); + } + bool hasPropertyOp = nodep->propp()->isMultiCycleSva(); + if (!hasPropertyOp) { + nodep->propp()->foreach([&](const AstNode* np) { + if (VN_IS(np, Implication) || VN_IS(np, Until) || VN_IS(np, PropSpec)) { + hasPropertyOp = true; + } + }); + } + if (hasPropertyOp) { + nodep->v3warn(E_UNSUPPORTED, + "Unsupported: property/sequence operator inside bounded" + " always (IEEE 1800-2023 16.12.11)"); + } + nodep->dtypeSetBit(); + } + } void visit(AstRising* nodep) override { assertAtExpr(nodep); if (m_vup->prelim()) { diff --git a/src/verilog.y b/src/verilog.y index 93ccefa49..d1923d820 100644 --- a/src/verilog.y +++ b/src/verilog.y @@ -6753,11 +6753,13 @@ pexpr: // IEEE: property_expr (The name pexpr is important as regex | yS_NEXTTIME '[' constExpr ']' pexpr %prec yS_NEXTTIME { $$ = $5; BBUNSUP($1, "Unsupported: s_nexttime[] (in property expression)"); DEL($3); } | yALWAYS pexpr - { $$ = $2; BBUNSUP($1, "Unsupported: always (in property expression)"); } - | yALWAYS anyrange pexpr %prec yALWAYS - { $$ = $3; BBUNSUP($1, "Unsupported: always[] (in property expression)"); DEL($2); } - | yS_ALWAYS anyrange pexpr %prec yS_ALWAYS - { $$ = $3; BBUNSUP($1, "Unsupported: s_always (in property expression)"); DEL($2); } + { $$ = $2; } + | yALWAYS '[' constExpr ':' constExpr ']' pexpr %prec yALWAYS + { $$ = new AstPropAlways{$1, $7, $3, $5, false}; } + | yS_ALWAYS '[' constExpr ':' constExpr ']' pexpr %prec yS_ALWAYS + { $$ = new AstPropAlways{$1, $7, $3, $5, true}; } + | yS_ALWAYS pexpr + { $$ = new AstPropAlways{$1, $2, new AstUnbounded{$1}, new AstUnbounded{$1}, true}; } | yS_EVENTUALLY pexpr { $$ = $2; BBUNSUP($1, "Unsupported: s_eventually (in property expression)"); } | yS_EVENTUALLY anyrange pexpr %prec yS_EVENTUALLY diff --git a/test_regress/t/t_assert_always_unsup.out b/test_regress/t/t_assert_always_unsup.out index 7bbe0404c..a339cc1fd 100644 --- a/test_regress/t/t_assert_always_unsup.out +++ b/test_regress/t/t_assert_always_unsup.out @@ -1,15 +1,9 @@ -%Error-UNSUPPORTED: t/t_assert_always_unsup.v:20:5: Unsupported: always[] (in property expression) - 20 | always [2:5] a; - | ^~~~~~ - ... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest -%Error-UNSUPPORTED: t/t_assert_always_unsup.v:24:5: Unsupported: s_always (in property expression) - 24 | s_always [2:5] a; - | ^~~~~~~~ -%Error-UNSUPPORTED: t/t_assert_always_unsup.v:28:5: Unsupported: eventually[] (in property expression) - 28 | eventually [2:5] a; +%Error-UNSUPPORTED: t/t_assert_always_unsup.v:20:5: Unsupported: eventually[] (in property expression) + 20 | eventually [2:5] a; | ^~~~~~~~~~ -%Error: t/t_assert_always_unsup.v:32:18: syntax error, unexpected ']', expecting ':' - 32 | eventually [2] a; + ... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest +%Error: t/t_assert_always_unsup.v:24:18: syntax error, unexpected ']', expecting ':' + 24 | eventually [2] a; | ^ ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. %Error: Cannot continue diff --git a/test_regress/t/t_assert_always_unsup.v b/test_regress/t/t_assert_always_unsup.v index 332064fee..cc1382e24 100644 --- a/test_regress/t/t_assert_always_unsup.v +++ b/test_regress/t/t_assert_always_unsup.v @@ -16,14 +16,6 @@ module t ( val = ~val; end - property p_alw; - always [2:5] a; - endproperty - - property p_s_alw; - s_always [2:5] a; - endproperty - property p_ev; eventually [2:5] a; endproperty diff --git a/test_regress/t/t_debug_emitv.out b/test_regress/t/t_debug_emitv.out index b7bea1967..005b185bc 100644 --- a/test_regress/t/t_debug_emitv.out +++ b/test_regress/t/t_debug_emitv.out @@ -650,6 +650,22 @@ module Vt_debug_emitv_t; $display("pass"); end end + begin : assert_prop_always + assert property (@(posedge clk) always ['sh0: + 'sh3] in + ) begin + end + else begin + end + end + begin : assert_prop_s_always + assert property (@(posedge clk) s_always ['sh1: + 'sh2] in + ) begin + end + else begin + end + end int signed a; int signed ao; initial begin diff --git a/test_regress/t/t_debug_emitv.v b/test_regress/t/t_debug_emitv.v index 9594487f1..4f94bd864 100644 --- a/test_regress/t/t_debug_emitv.v +++ b/test_regress/t/t_debug_emitv.v @@ -330,6 +330,9 @@ module t (/*AUTOARG*/ cover_concurrent: cover property(prop); cover_concurrent_stmt: cover property(prop) $display("pass"); + assert_prop_always: assert property (@(posedge clk) always [0:3] in); + assert_prop_s_always: assert property (@(posedge clk) s_always [1:2] in); + int a; int ao; diff --git a/test_regress/t/t_prop_always.py b/test_regress/t/t_prop_always.py new file mode 100755 index 000000000..2351d6963 --- /dev/null +++ b/test_regress/t/t_prop_always.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.compile() + +test.execute() + +test.passes() diff --git a/test_regress/t/t_prop_always.v b/test_regress/t/t_prop_always.v new file mode 100644 index 000000000..3f54b1023 --- /dev/null +++ b/test_regress/t/t_prop_always.v @@ -0,0 +1,107 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +// verilog_format: on + +module t ( + input clk +); + + bit [63:0] crc = 64'h5aef0c8d_d70a4497; + int cyc = 0; + logic a_high = 1'b1; + logic a_low = 1'b0; + wire a_rand = crc[0]; + wire rst_rand = crc[5]; + + // Per-assertion queues record the simulation cyc on each pass / else fire. + // For "always [m:n] P" the action runs at cyc=K+n on success and at the + // detected-violation cyc on failure -- both deterministic given the inputs. + int high_bounded_pass_q[$]; + int high_sbounded_pass_q[$]; + int high_degenerate_pass_q[$]; + int low_bounded_fail_q[$]; + int low_degenerate_fail_q[$]; + int rand_bounded_pass_q[$]; + int rand_bounded_fail_q[$]; + int disable_bounded_pass_q[$]; + int disable_bounded_fail_q[$]; + + // Bare always (collapses to immediate P). + assert property (@(posedge clk) always 1'b1); + + // Bounded weak always over constant-true input. + assert property (@(posedge clk) always [0:3] a_high) high_bounded_pass_q.push_back(cyc); + + // Bounded strong s_always over constant-true input. + assert property (@(posedge clk) s_always [1:2] a_high) high_sbounded_pass_q.push_back(cyc); + + // Degenerate [0:0]: equivalent to immediate sample. + assert property (@(posedge clk) always [0:0] a_high) high_degenerate_pass_q.push_back(cyc); + + // Constant-false: every attempt fails. + assert property (@(posedge clk) always [0:3] a_low) + ; + else low_bounded_fail_q.push_back(cyc); + + assert property (@(posedge clk) always [0:0] a_low) + ; + else low_degenerate_fail_q.push_back(cyc); + + // CRC-driven random input: window [cyc..cyc+3] of a_rand. + assert property (@(posedge clk) always [0:3] a_rand) rand_bounded_pass_q.push_back(cyc); + else rand_bounded_fail_q.push_back(cyc); + + // disable iff suppresses attempts whose start cyc has rst_rand=1. + assert property (@(posedge clk) disable iff (rst_rand) always [0:3] a_rand) + disable_bounded_pass_q.push_back(cyc); + else disable_bounded_fail_q.push_back(cyc); + + // Bare always inside named property. + property p_always_true; + @(posedge clk) always (1'b1); + endproperty + assert property (p_always_true); + + // disable iff inside named property. + property p_disable_named; + @(posedge clk) disable iff (rst_rand) always [1:2] a_high; + endproperty + assert property (p_disable_named); + + always @(posedge clk) begin + cyc <= cyc + 1; + crc <= {crc[62:0], crc[63] ^ crc[2] ^ crc[0]}; + if (cyc == 19) begin + // Constant-true window [0:3]: K=0..16 succeed at cyc K+3 = 3..19. + `checkd(high_bounded_pass_q.size(), 17); + `checkd(high_bounded_pass_q[0], 3); + `checkd(high_bounded_pass_q[$], 19); + // Strong [1:2]: K=0..17 succeed at cyc K+2 = 2..19. + `checkd(high_sbounded_pass_q.size(), 18); + `checkd(high_sbounded_pass_q[0], 2); + `checkd(high_sbounded_pass_q[$], 19); + // Degenerate [0:0]: K=0..19 succeed at cyc K = 0..19. + `checkd(high_degenerate_pass_q.size(), 20); + `checkd(high_degenerate_pass_q[0], 0); + `checkd(high_degenerate_pass_q[$], 19); + // Constant-false: every attempt fails immediately. + `checkd(low_bounded_fail_q.size(), 20); + `checkd(low_degenerate_fail_q.size(), 20); + // CRC + disable streams: counts pinned (cross-checked against Questa). + `checkd(rand_bounded_pass_q.size(), 0); + `checkd(rand_bounded_fail_q.size(), 20); + `checkd(disable_bounded_pass_q.size(), 0); + `checkd(disable_bounded_fail_q.size(), 13); + $write("*-* All Finished *-*\n"); + $finish; + end + end + +endmodule diff --git a/test_regress/t/t_prop_always_bad.out b/test_regress/t/t_prop_always_bad.out new file mode 100644 index 000000000..4e06a8feb --- /dev/null +++ b/test_regress/t/t_prop_always_bad.out @@ -0,0 +1,34 @@ +%Error: t/t_prop_always_bad.v:13:20: always range low bound must be non-negative (IEEE 1800-2023 16.12.11) + : ... note: In instance 't' + 13 | assert property (always [-1:3] a); + | ^~~~~~ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Error: t/t_prop_always_bad.v:14:20: always range high bound must be >= low bound (IEEE 1800-2023 16.12.11) + : ... note: In instance 't' + 14 | assert property (always [5:2] a); + | ^~~~~~ +%Error: t/t_prop_always_bad.v:15:28: Expecting expression to be constant, but variable isn't const: 'x' + : ... note: In instance 't' + 15 | assert property (always [x:3] a); + | ^ +%Error: t/t_prop_always_bad.v:15:20: always range low bound must be a constant expression (IEEE 1800-2023 16.12.11) + : ... note: In instance 't' + 15 | assert property (always [x:3] a); + | ^~~~~~ +%Error: t/t_prop_always_bad.v:16:30: Expecting expression to be constant, but variable isn't const: 'x' + : ... note: In instance 't' + 16 | assert property (always [1:x] a); + | ^ +%Error: t/t_prop_always_bad.v:16:20: always range high bound must be a constant expression (IEEE 1800-2023 16.12.11) + : ... note: In instance 't' + 16 | assert property (always [1:x] a); + | ^~~~~~ +%Error: t/t_prop_always_bad.v:17:20: s_always range must be bounded (IEEE 1800-2023 16.12.11) + : ... note: In instance 't' + 17 | assert property (s_always a); + | ^~~~~~~~ +%Error: t/t_prop_always_bad.v:18:20: s_always range must be bounded (IEEE 1800-2023 16.12.11) + : ... note: In instance 't' + 18 | assert property (s_always [1:$] a); + | ^~~~~~~~ +%Error: Exiting due to diff --git a/test_regress/t/t_prop_always_bad.py b/test_regress/t/t_prop_always_bad.py new file mode 100755 index 000000000..77a0ac64b --- /dev/null +++ b/test_regress/t/t_prop_always_bad.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.lint(expect_filename=test.golden_filename, fails=True) + +test.passes() diff --git a/test_regress/t/t_prop_always_bad.v b/test_regress/t/t_prop_always_bad.v new file mode 100644 index 000000000..8fa30b3eb --- /dev/null +++ b/test_regress/t/t_prop_always_bad.v @@ -0,0 +1,20 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +module t (input clk); + int x; + logic a; + + default clocking cb @(posedge clk); endclocking + + assert property (always [-1:3] a); + assert property (always [5:2] a); + assert property (always [x:3] a); + assert property (always [1:x] a); + assert property (s_always a); + assert property (s_always [1:$] a); + +endmodule diff --git a/test_regress/t/t_prop_always_unsup.out b/test_regress/t/t_prop_always_unsup.out new file mode 100644 index 000000000..52bd14918 --- /dev/null +++ b/test_regress/t/t_prop_always_unsup.out @@ -0,0 +1,18 @@ +%Error-UNSUPPORTED: t/t_prop_always_unsup.v:12:35: Unsupported: unbounded always range (always [m:$]) + : ... note: In instance 't' + 12 | assert property (@(posedge clk) always [2:$] a); + | ^~~~~~ + ... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest +%Error-UNSUPPORTED: t/t_prop_always_unsup.v:15:35: Unsupported: property/sequence operator inside bounded always (IEEE 1800-2023 16.12.11) + : ... note: In instance 't' + 15 | assert property (@(posedge clk) always [0:3] (a |-> b)); + | ^~~~~~ +%Error-UNSUPPORTED: t/t_prop_always_unsup.v:16:35: Unsupported: property/sequence operator inside bounded always (IEEE 1800-2023 16.12.11) + : ... note: In instance 't' + 16 | assert property (@(posedge clk) always [0:3] (a |=> b)); + | ^~~~~~ +%Error-UNSUPPORTED: t/t_prop_always_unsup.v:17:35: Unsupported: property/sequence operator inside bounded always (IEEE 1800-2023 16.12.11) + : ... note: In instance 't' + 17 | assert property (@(posedge clk) always [0:3] (a ##1 b)); + | ^~~~~~ +%Error: Exiting due to diff --git a/test_regress/t/t_prop_always_unsup.py b/test_regress/t/t_prop_always_unsup.py new file mode 100755 index 000000000..77a0ac64b --- /dev/null +++ b/test_regress/t/t_prop_always_unsup.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.lint(expect_filename=test.golden_filename, fails=True) + +test.passes() diff --git a/test_regress/t/t_prop_always_unsup.v b/test_regress/t/t_prop_always_unsup.v new file mode 100644 index 000000000..7013c9fff --- /dev/null +++ b/test_regress/t/t_prop_always_unsup.v @@ -0,0 +1,19 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +module t (input clk); + logic a; + logic b; + + // IEEE-legal but engine has no sim-end liveness. + assert property (@(posedge clk) always [2:$] a); + + // Nested sequence/property operators inside bounded always. + assert property (@(posedge clk) always [0:3] (a |-> b)); + assert property (@(posedge clk) always [0:3] (a |=> b)); + assert property (@(posedge clk) always [0:3] (a ##1 b)); + +endmodule diff --git a/test_regress/t/t_property_pexpr_unsup.out b/test_regress/t/t_property_pexpr_unsup.out index 0a93880ae..a021411dc 100644 --- a/test_regress/t/t_property_pexpr_unsup.out +++ b/test_regress/t/t_property_pexpr_unsup.out @@ -23,21 +23,12 @@ %Error-UNSUPPORTED: t/t_property_pexpr_unsup.v:63:5: Unsupported: s_nexttime[] (in property expression) 63 | s_nexttime [2] a; | ^~~~~~~~~~ -%Error-UNSUPPORTED: t/t_property_pexpr_unsup.v:67:14: Unsupported: always (in property expression) - 67 | nexttime always a; - | ^~~~~~ %Error-UNSUPPORTED: t/t_property_pexpr_unsup.v:67:5: Unsupported: nexttime (in property expression) 67 | nexttime always a; | ^~~~~~~~ -%Error-UNSUPPORTED: t/t_property_pexpr_unsup.v:71:18: Unsupported: always (in property expression) - 71 | nexttime [2] always a; - | ^~~~~~ %Error-UNSUPPORTED: t/t_property_pexpr_unsup.v:71:5: Unsupported: nexttime[] (in property expression) 71 | nexttime [2] always a; | ^~~~~~~~ -%Error-UNSUPPORTED: t/t_property_pexpr_unsup.v:75:18: Unsupported: always (in property expression) - 75 | nexttime [2] always a; - | ^~~~~~ %Error-UNSUPPORTED: t/t_property_pexpr_unsup.v:75:5: Unsupported: nexttime[] (in property expression) 75 | nexttime [2] always a; | ^~~~~~~~ @@ -47,9 +38,6 @@ %Error-UNSUPPORTED: t/t_property_pexpr_unsup.v:79:5: Unsupported: nexttime (in property expression) 79 | nexttime s_eventually a; | ^~~~~~~~ -%Error-UNSUPPORTED: t/t_property_pexpr_unsup.v:83:33: Unsupported: always (in property expression) - 83 | nexttime s_eventually [2:$] always a; - | ^~~~~~ %Error-UNSUPPORTED: t/t_property_pexpr_unsup.v:83:14: Unsupported: s_eventually[] (in property expression) 83 | nexttime s_eventually [2:$] always a; | ^~~~~~~~~~~~ diff --git a/test_regress/t/t_property_unsup.out b/test_regress/t/t_property_unsup.out index ec5a79c63..d6dda24c3 100644 --- a/test_regress/t/t_property_unsup.out +++ b/test_regress/t/t_property_unsup.out @@ -5,58 +5,40 @@ %Error-UNSUPPORTED: t/t_property_unsup.v:80:20: Unsupported: eventually[] (in property expression) 80 | assert property (eventually[0: 2] counter == 3); | ^~~~~~~~~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:90:21: Unsupported: always (in property expression) - 90 | assert property ((always a) implies (always a)); - | ^~~~~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:90:40: Unsupported: always (in property expression) - 90 | assert property ((always a) implies (always a)); - | ^~~~~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:92:26: Unsupported: always (in property expression) - 92 | assert property ((a or(always b)) implies (a or(always b))); - | ^~~~~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:92:51: Unsupported: always (in property expression) - 92 | assert property ((a or(always b)) implies (a or(always b))); - | ^~~~~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:94:21: Unsupported: eventually[] (in property expression) - 94 | assert property ((eventually[0: 1] a) implies (eventually[0: 1] a)); +%Error-UNSUPPORTED: t/t_property_unsup.v:90:21: Unsupported: eventually[] (in property expression) + 90 | assert property ((eventually[0: 1] a) implies (eventually[0: 1] a)); | ^~~~~~~~~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:94:50: Unsupported: eventually[] (in property expression) - 94 | assert property ((eventually[0: 1] a) implies (eventually[0: 1] a)); +%Error-UNSUPPORTED: t/t_property_unsup.v:90:50: Unsupported: eventually[] (in property expression) + 90 | assert property ((eventually[0: 1] a) implies (eventually[0: 1] a)); | ^~~~~~~~~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:96:21: Unsupported: s_eventually (in property expression) - 96 | assert property ((s_eventually a) implies (s_eventually a)); +%Error-UNSUPPORTED: t/t_property_unsup.v:92:21: Unsupported: s_eventually (in property expression) + 92 | assert property ((s_eventually a) implies (s_eventually a)); | ^~~~~~~~~~~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:96:46: Unsupported: s_eventually (in property expression) - 96 | assert property ((s_eventually a) implies (s_eventually a)); +%Error-UNSUPPORTED: t/t_property_unsup.v:92:46: Unsupported: s_eventually (in property expression) + 92 | assert property ((s_eventually a) implies (s_eventually a)); | ^~~~~~~~~~~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:106:23: Unsupported: #-# (in property expression) - 106 | assert property ((a #-# b) implies (a #-# b)); +%Error-UNSUPPORTED: t/t_property_unsup.v:102:23: Unsupported: #-# (in property expression) + 102 | assert property ((a #-# b) implies (a #-# b)); | ^~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:106:41: Unsupported: #-# (in property expression) - 106 | assert property ((a #-# b) implies (a #-# b)); +%Error-UNSUPPORTED: t/t_property_unsup.v:102:41: Unsupported: #-# (in property expression) + 102 | assert property ((a #-# b) implies (a #-# b)); | ^~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:116:21: Unsupported: always (in property expression) - 116 | assert property ((always a) iff (always a)); - | ^~~~~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:116:36: Unsupported: always (in property expression) - 116 | assert property ((always a) iff (always a)); - | ^~~~~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:118:21: Unsupported: eventually[] (in property expression) - 118 | assert property ((eventually[0: 1] a) iff (eventually[0: 1] a)); +%Error-UNSUPPORTED: t/t_property_unsup.v:112:21: Unsupported: eventually[] (in property expression) + 112 | assert property ((eventually[0: 1] a) iff (eventually[0: 1] a)); | ^~~~~~~~~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:118:46: Unsupported: eventually[] (in property expression) - 118 | assert property ((eventually[0: 1] a) iff (eventually[0: 1] a)); +%Error-UNSUPPORTED: t/t_property_unsup.v:112:46: Unsupported: eventually[] (in property expression) + 112 | assert property ((eventually[0: 1] a) iff (eventually[0: 1] a)); | ^~~~~~~~~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:120:21: Unsupported: s_eventually (in property expression) - 120 | assert property ((s_eventually a) iff (s_eventually a)); +%Error-UNSUPPORTED: t/t_property_unsup.v:114:21: Unsupported: s_eventually (in property expression) + 114 | assert property ((s_eventually a) iff (s_eventually a)); | ^~~~~~~~~~~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:120:42: Unsupported: s_eventually (in property expression) - 120 | assert property ((s_eventually a) iff (s_eventually a)); +%Error-UNSUPPORTED: t/t_property_unsup.v:114:42: Unsupported: s_eventually (in property expression) + 114 | assert property ((s_eventually a) iff (s_eventually a)); | ^~~~~~~~~~~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:130:23: Unsupported: #-# (in property expression) - 130 | assert property ((a #-# b) iff (a #-# b)); +%Error-UNSUPPORTED: t/t_property_unsup.v:124:23: Unsupported: #-# (in property expression) + 124 | assert property ((a #-# b) iff (a #-# b)); | ^~~ -%Error-UNSUPPORTED: t/t_property_unsup.v:130:37: Unsupported: #-# (in property expression) - 130 | assert property ((a #-# b) iff (a #-# b)); +%Error-UNSUPPORTED: t/t_property_unsup.v:124:37: Unsupported: #-# (in property expression) + 124 | assert property ((a #-# b) iff (a #-# b)); | ^~~ %Error: Exiting due to diff --git a/test_regress/t/t_property_unsup.v b/test_regress/t/t_property_unsup.v index e496b28c9..e57ee2485 100644 --- a/test_regress/t/t_property_unsup.v +++ b/test_regress/t/t_property_unsup.v @@ -86,10 +86,6 @@ module sva_implies2 ( b ); - p0 : - assert property ((always a) implies (always a)); - p1 : - assert property ((a or(always b)) implies (a or(always b))); p2 : assert property ((eventually[0: 1] a) implies (eventually[0: 1] a)); p3 : @@ -112,8 +108,6 @@ module sva_iff2 ( b ); - p0 : - assert property ((always a) iff (always a)); p1 : assert property ((eventually[0: 1] a) iff (eventually[0: 1] a)); p2 : From cee174c4b6d2d40493aadbc6ce366276839426a3 Mon Sep 17 00:00:00 2001 From: Geza Lore Date: Mon, 27 Apr 2026 16:58:38 +0100 Subject: [PATCH 006/350] Fix inlining static initializer in V3Gate (#7503) V3Gate could incorrectly inline a non-constant static initializer of a variable into the use site if there are no other drivers. Disabling. Fixes #5381 --- src/V3AstInlines.h | 1 + src/V3AstNodeOther.h | 1 + src/V3Gate.cpp | 11 +++++++++++ test_regress/t/t_struct_initial_assign.py | 2 +- test_regress/t/t_struct_initial_assign.v | 4 +--- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/V3AstInlines.h b/src/V3AstInlines.h index e31960429..8a4477060 100644 --- a/src/V3AstInlines.h +++ b/src/V3AstInlines.h @@ -146,6 +146,7 @@ bool AstBasicDType::ascending() const { bool AstActive::hasClocked() const { return m_sentreep->hasClocked(); } bool AstActive::hasCombo() const { return m_sentreep->hasCombo(); } +bool AstActive::hasStatic() const { return m_sentreep->hasStatic(); } AstAlways::AstAlways(AstAssignW* assignp) : ASTGEN_SUPER_Always(assignp->fileline(), assignp) diff --git a/src/V3AstNodeOther.h b/src/V3AstNodeOther.h index 32a38ca0b..93cc01fdc 100644 --- a/src/V3AstNodeOther.h +++ b/src/V3AstNodeOther.h @@ -432,6 +432,7 @@ public: // METHODS inline bool hasClocked() const; inline bool hasCombo() const; + inline bool hasStatic() const; }; class AstAlias final : public AstNode { // Alias construct - Used for source level net alias, and also for variable aliases internally diff --git a/src/V3Gate.cpp b/src/V3Gate.cpp index ca15a0bb6..e24fd54ef 100644 --- a/src/V3Gate.cpp +++ b/src/V3Gate.cpp @@ -47,6 +47,7 @@ class GateEitherVertex VL_NOT_FINAL : public V3GraphVertex { bool m_reducible = true; // True if this node should be able to be eliminated bool m_dedupable = true; // True if this node should be able to be deduped bool m_consumed = false; // Output goes to something meaningful + bool m_staticInit = false; // True if this node is a static initializer public: explicit GateEitherVertex(V3Graph* graphp) : V3GraphVertex{graphp} {} @@ -56,10 +57,12 @@ public: bool reducible() const { return m_reducible; } bool dedupable() const { return m_dedupable; } bool consumed() const { return m_consumed; } + bool staticInit() const { return m_staticInit; } void setConsumed(const char* /*consumedReason*/) { // if (!m_consumed) UINFO(0, "\t\tSetConsumed " << consumedReason << " " << this); m_consumed = true; } + void setStaticInit() { m_staticInit = true; } void clearReducible(const char* /*nonReducibleReason*/) { // UINFO(0, " NR: " << nonReducibleReason << " " << name()); m_reducible = false; @@ -196,6 +199,7 @@ class GateBuildVisitor final : public VNVisitorConst { const AstScope* m_scopep = nullptr; // Current scope being processed AstActive* m_activep = nullptr; // Current active bool m_inClockedActive = false; // Underneath clocked active + bool m_inStaticActive = false; // Underneath static active bool m_inSenItem = false; // Underneath AstSenItem; any varrefs are clocks // METHODS @@ -219,6 +223,8 @@ class GateBuildVisitor final : public VNVisitorConst { m_logicVertexp->clearReducibleAndDedupable(nonReducibleReason); } else if (m_inClockedActive) { m_logicVertexp->clearReducible("Clocked logic"); // but dedupable + } else if (m_inStaticActive) { + m_logicVertexp->setStaticInit(); } if (consumeReason) m_logicVertexp->setConsumed(consumeReason); checkNode(nodep); @@ -242,8 +248,10 @@ class GateBuildVisitor final : public VNVisitorConst { UASSERT_OBJ(!m_activep, nodep, "Should not nest"); VL_RESTORER(m_activep); VL_RESTORER(m_inClockedActive); + VL_RESTORER(m_inStaticActive); m_activep = nodep; m_inClockedActive = nodep->hasClocked(); + m_inStaticActive = nodep->hasStatic(); // AstVarScope::user2 -> bool: Signal used in SenItem in *this* active block const VNUser2InUse user2InUse; @@ -700,6 +708,9 @@ class GateInline final { if (!VN_IS(substp, NodeVarRef)) continue; } + // Only inline static initializer if constant known at compile time + if (lVtxp->staticInit() && !VN_IS(substp, Const)) continue; + // Process it ++m_statInlined; diff --git a/test_regress/t/t_struct_initial_assign.py b/test_regress/t/t_struct_initial_assign.py index f3c898f62..c1140f359 100755 --- a/test_regress/t/t_struct_initial_assign.py +++ b/test_regress/t/t_struct_initial_assign.py @@ -13,6 +13,6 @@ test.scenarios('simulator') test.compile(verilator_flags2=["--timing"]) -test.execute(fails=test.vlt_all) # Issue #5381 +test.execute() test.passes() diff --git a/test_regress/t/t_struct_initial_assign.v b/test_regress/t/t_struct_initial_assign.v index a72732926..56c6aaf26 100644 --- a/test_regress/t/t_struct_initial_assign.v +++ b/test_regress/t/t_struct_initial_assign.v @@ -15,7 +15,7 @@ module t ( input clk ); - str_t bar_in; + str_t bar_in = '0; // Set bar_in to 0 initially str_t bar_out; Sub sub ( @@ -28,8 +28,6 @@ module t ( integer i; str_t initOut; - bar_in = '0; // Set bar_in to 0 initially - // Wait for the first falling edge of the clock @(negedge clk); From 4437ae79e77873350ccd72e51fa90623f313f2b8 Mon Sep 17 00:00:00 2001 From: Nikolay Puzanov Date: Mon, 27 Apr 2026 23:58:30 +0300 Subject: [PATCH 007/350] Fix virtual interface method call inlining and IMPURE suppression (#7505) --- src/V3Task.cpp | 78 +++++++++++++------ .../t/t_interface_func_impure_bad.out | 9 +++ test_regress/t/t_interface_func_impure_bad.py | 16 ++++ test_regress/t/t_interface_func_impure_bad.v | 34 ++++++++ ...interface_virtual_func_runtime_instance.py | 18 +++++ ..._interface_virtual_func_runtime_instance.v | 40 ++++++++++ .../t_interface_virtual_func_static_direct.py | 18 +++++ .../t_interface_virtual_func_static_direct.v | 28 +++++++ 8 files changed, 219 insertions(+), 22 deletions(-) create mode 100644 test_regress/t/t_interface_func_impure_bad.out create mode 100644 test_regress/t/t_interface_func_impure_bad.py create mode 100644 test_regress/t/t_interface_func_impure_bad.v create mode 100755 test_regress/t/t_interface_virtual_func_runtime_instance.py create mode 100644 test_regress/t/t_interface_virtual_func_runtime_instance.v create mode 100755 test_regress/t/t_interface_virtual_func_static_direct.py create mode 100644 test_regress/t/t_interface_virtual_func_static_direct.v diff --git a/src/V3Task.cpp b/src/V3Task.cpp index 6c9a5578e..fba7690f8 100644 --- a/src/V3Task.cpp +++ b/src/V3Task.cpp @@ -61,6 +61,7 @@ class TaskFTaskVertex final : public TaskBaseVertex { // Every task gets a vertex, and we link tasks together based on funcrefs. AstNodeFTask* const m_nodep; AstCFunc* m_cFuncp = nullptr; + bool m_needsNonInlineCFunc = false; // Needs CFunc for selected non-inlined call sites public: TaskFTaskVertex(V3Graph* graphp, AstNodeFTask* nodep) @@ -72,6 +73,8 @@ public: string dotColor() const override { return pure() ? "black" : "red"; } AstCFunc* cFuncp() const { return m_cFuncp; } void cFuncp(AstCFunc* nodep) { m_cFuncp = nodep; } + bool needsNonInlineCFunc() const { return m_needsNonInlineCFunc; } + void needsNonInlineCFunc(bool flag) { m_needsNonInlineCFunc = flag; } }; class TaskCodeVertex final : public TaskBaseVertex { @@ -119,6 +122,17 @@ class TaskStateVisitor final : public VNVisitor { public: // METHODS + static bool isVirtualIfaceMethodCall(const AstNodeFTaskRef* refp) { + const AstMethodCall* const mrefp = VN_CAST(refp, MethodCall); + if (!mrefp) return false; + const AstNodeExpr* const fromp = mrefp->fromp(); + if (!fromp || !fromp->dtypep()) return false; + const AstIfaceRefDType* const ifrefp = VN_CAST(fromp->dtypep()->skipRefp(), IfaceRefDType); + return ifrefp && ifrefp->isVirtual(); + } + static bool isIfaceFTaskScope(const AstScope* scopep) { + return scopep && VN_IS(scopep->modp(), Iface); + } AstScope* getScope(AstNodeFTask* nodep) { AstScope* const scopep = VN_AS(nodep->user3p(), Scope); UASSERT_OBJ(scopep, nodep, "No scope for function"); @@ -138,6 +152,9 @@ public: m_funcToClassMap[newp] = getClassp(nodep); } bool ftaskNoInline(AstNodeFTask* nodep) { return getFTaskVertex(nodep)->noInline(); } + bool ftaskNeedsNonInlineCFunc(AstNodeFTask* nodep) { + return getFTaskVertex(nodep)->needsNonInlineCFunc(); + } AstCFunc* ftaskCFuncp(AstNodeFTask* nodep) { return getFTaskVertex(nodep)->cFuncp(); } void ftaskCFuncp(AstNodeFTask* nodep, AstCFunc* cfuncp) { getFTaskVertex(nodep)->cFuncp(cfuncp); @@ -229,6 +246,9 @@ private: UASSERT_OBJ(nodep->taskp(), nodep, "Unlinked task"); TaskFTaskVertex* const taskVtxp = getFTaskVertex(nodep->taskp()); new TaskEdge{&m_callGraph, m_curVxp, taskVtxp}; + if (isVirtualIfaceMethodCall(nodep) && isIfaceFTaskScope(getScope(nodep->taskp()))) { + taskVtxp->needsNonInlineCFunc(true); + } // Do we have to disable inlining the function? const V3TaskConnects tconnects = V3Task::taskConnects(nodep, nodep->taskp()->stmtsp()); if (!taskVtxp->noInline()) { // Else short-circuit below @@ -251,13 +271,6 @@ private: if (nodep->dpiImport()) m_curVxp->noInline(true); if (nodep->classMethod()) m_curVxp->noInline(true); // Until V3Task supports it if (nodep->recursive()) m_curVxp->noInline(true); - // V3Scope resolves virtual-interface MethodCalls via user2p (last-wins), - // so inlining would bake in the wrong instance's VarScope refs. - if (v3Global.hasVirtIfaces()) { - if (const AstScope* const scopep = VN_CAST(nodep->user3p(), Scope)) { - if (VN_IS(scopep->modp(), Iface)) m_curVxp->noInline(true); - } - } if (nodep->isConstructor()) { m_curVxp->noInline(true); m_ctorp = nodep; @@ -1495,8 +1508,10 @@ class TaskVisitor final : public VNVisitor { // Iterate into the FTask we are calling. Note it may be under a different // scope then the caller, so we need to restore state. VL_RESTORER(m_scopep); + VL_RESTORER(m_modp); VL_RESTORER(m_insStmtp); m_scopep = m_statep->getScope(nodep); + m_modp = m_scopep->modp(); iterate(nodep); } void insertBeforeStmt(AstNode* nodep, AstNode* newp) { @@ -1528,6 +1543,16 @@ class TaskVisitor final : public VNVisitor { } } + bool isIfaceLocalImpure(AstNodeFTask* nodep, AstNode* impurep) { + const AstScope* const scopep = m_statep->getScope(nodep); + if (!TaskStateVisitor::isIfaceFTaskScope(scopep)) return false; + + const AstVarRef* const refp = VN_CAST(impurep, VarRef); + if (!refp || !refp->varScopep()) return false; + + return refp->varScopep()->scopep() == scopep; + } + // VISITORS void visit(AstNodeModule* nodep) override { VL_RESTORER(m_modp); @@ -1598,7 +1623,10 @@ class TaskVisitor final : public VNVisitor { // Create cloned statements AstNode* beginp; AstCNew* cnewp = nullptr; - if (m_statep->ftaskNoInline(nodep->taskp())) { + const bool virtualIfaceCall + = TaskStateVisitor::isVirtualIfaceMethodCall(nodep) + && TaskStateVisitor::isIfaceFTaskScope(m_statep->getScope(nodep->taskp())); + if (m_statep->ftaskNoInline(nodep->taskp()) || virtualIfaceCall) { processArgs(nodep); // This may share VarScope's with a public task, if any. Yuk. beginp = createNonInlinedFTask(nodep, namePrefix, outvscp, cnewp /*ref*/); @@ -1697,32 +1725,38 @@ class TaskVisitor final : public VNVisitor { } const bool noInline = m_statep->ftaskNoInline(nodep); + const bool needsNonInlineCFunc = m_statep->ftaskNeedsNonInlineCFunc(nodep); // Warn if not inlining an impure ftask (unless method, recursive, - // or interface function -- interface member access is not truly external). + // or interface-local member access, which is not truly external). // Will likely not schedule correctly. // TODO: Why not if recursive? It will not work ... - if (noInline && !nodep->classMethod() && !nodep->recursive() - && !VN_IS(m_modp, Iface)) { + if (noInline && !nodep->classMethod() && !nodep->recursive()) { if (AstNode* const impurep = m_statep->checkImpure(nodep)) { - nodep->v3warn( - IMPURE, - "Unsupported: External variable referenced by non-inlined function/task: " - << nodep->prettyNameQ() << '\n' - << nodep->warnContextPrimary() << '\n' - << impurep->warnOther() << "... Location of the external reference: " - << impurep->prettyNameQ() << '\n' - << impurep->warnContextSecondary()); + if (!isIfaceLocalImpure(nodep, impurep)) { + nodep->v3warn( + IMPURE, + "Unsupported: External variable referenced by non-inlined function/task: " + << nodep->prettyNameQ() << '\n' + << nodep->warnContextPrimary() << '\n' + << impurep->warnOther() << "... Location of the external reference: " + << impurep->prettyNameQ() << '\n' + << impurep->warnContextSecondary()); + } } } - if (noInline || nodep->dpiImport() || nodep->dpiExport() || nodep->taskPublic()) { + if (noInline || needsNonInlineCFunc || nodep->dpiImport() || nodep->dpiExport() + || nodep->taskPublic()) { // Clone it first, because we may have later FTaskRef's that still need // the original version. AstNodeFTask* const clonedFuncp = nodep->cloneTree(false); if (nodep->isConstructor()) m_statep->remapFuncClassp(nodep, clonedFuncp); - if (AstCFunc* const cfuncp = makeUserFunc(clonedFuncp, noInline)) { + const bool makeNonInlineFunc = noInline || needsNonInlineCFunc; + if (AstCFunc* const cfuncp = makeUserFunc(clonedFuncp, makeNonInlineFunc)) { nodep->addNextHere(cfuncp); - if (nodep->dpiImport() || noInline) m_statep->ftaskCFuncp(nodep, cfuncp); + if (nodep->dpiImport() || makeNonInlineFunc) { + m_statep->ftaskCFuncp(nodep, cfuncp); + } iterateIntoFTask(clonedFuncp); // Do the clone too } } diff --git a/test_regress/t/t_interface_func_impure_bad.out b/test_regress/t/t_interface_func_impure_bad.out new file mode 100644 index 000000000..70f39be73 --- /dev/null +++ b/test_regress/t/t_interface_func_impure_bad.out @@ -0,0 +1,9 @@ +%Error-IMPURE: t/t_interface_func_impure_bad.v:21:8: Unsupported: External variable referenced by non-inlined function/task: 'external_write' + : ... note: In instance 't.i' + 21 | task external_write; + | ^~~~~~~~~~~~~~ + t/t_interface_func_impure_bad.v:23:5: ... Location of the external reference: 'external_sig' + 23 | external_sig = 1'b1; + | ^~~~~~~~~~~~ + ... For error description see https://verilator.org/warn/IMPURE?v=latest +%Error: Exiting due to diff --git a/test_regress/t/t_interface_func_impure_bad.py b/test_regress/t/t_interface_func_impure_bad.py new file mode 100644 index 000000000..344a4e20a --- /dev/null +++ b/test_regress/t/t_interface_func_impure_bad.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_interface_func_impure_bad.v b/test_regress/t/t_interface_func_impure_bad.v new file mode 100644 index 000000000..6ef962e15 --- /dev/null +++ b/test_regress/t/t_interface_func_impure_bad.v @@ -0,0 +1,34 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +package pkg; + bit external_sig; +endpackage + +interface iface; + import pkg::*; + + bit local_sig; + + task local_write; + // verilator no_inline_task + local_sig = 1'b1; + endtask + + task external_write; + // verilator no_inline_task + external_sig = 1'b1; + endtask +endinterface + +module t; + iface i(); + + initial begin + i.local_write(); + i.external_write(); + end +endmodule diff --git a/test_regress/t/t_interface_virtual_func_runtime_instance.py b/test_regress/t/t_interface_virtual_func_runtime_instance.py new file mode 100755 index 000000000..6fe7d000c --- /dev/null +++ b/test_regress/t/t_interface_virtual_func_runtime_instance.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=["--binary"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_interface_virtual_func_runtime_instance.v b/test_regress/t/t_interface_virtual_func_runtime_instance.v new file mode 100644 index 000000000..1ad4e60e0 --- /dev/null +++ b/test_regress/t/t_interface_virtual_func_runtime_instance.v @@ -0,0 +1,40 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +interface iface; + bit x; + + function bit get_and_toggle(); + return x++; + endfunction +endinterface + +class Driver; + virtual iface vif; + + function bit call(); + return vif.get_and_toggle(); + endfunction +endclass + +module t; + iface a(); + iface b(); + + initial begin + automatic Driver d = new; + a.x = 1'b0; + b.x = 1'b1; + d.vif = a; + if (d.call() !== 1'b0) $stop; + d.vif = b; + if (d.call() !== 1'b1) $stop; + if (a.x !== 1'b1) $stop; + if (b.x !== 1'b0) $stop; + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_interface_virtual_func_static_direct.py b/test_regress/t/t_interface_virtual_func_static_direct.py new file mode 100755 index 000000000..6fe7d000c --- /dev/null +++ b/test_regress/t/t_interface_virtual_func_static_direct.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=["--binary"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_interface_virtual_func_static_direct.v b/test_regress/t/t_interface_virtual_func_static_direct.v new file mode 100644 index 000000000..6d8d6cd12 --- /dev/null +++ b/test_regress/t/t_interface_virtual_func_static_direct.v @@ -0,0 +1,28 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +interface iface; + function bit get(); + static bit x; + return x++; + endfunction +endinterface + +class Holder; + virtual iface vif; +endclass + +module t; + iface i(); + + initial begin + automatic Holder h = new; + if (i.get() !== 1'b0) $stop; + if (i.get() !== 1'b1) $stop; + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule From c460f0e6a03224119be8453f64daa82218a2856d Mon Sep 17 00:00:00 2001 From: github action Date: Mon, 27 Apr 2026 21:05:49 +0000 Subject: [PATCH 008/350] Apply 'make format' --- src/V3Task.cpp | 16 ++++++++-------- test_regress/t/t_interface_func_impure_bad.py | 0 2 files changed, 8 insertions(+), 8 deletions(-) mode change 100644 => 100755 test_regress/t/t_interface_func_impure_bad.py diff --git a/src/V3Task.cpp b/src/V3Task.cpp index fba7690f8..bea9cfdbd 100644 --- a/src/V3Task.cpp +++ b/src/V3Task.cpp @@ -1733,14 +1733,14 @@ class TaskVisitor final : public VNVisitor { if (noInline && !nodep->classMethod() && !nodep->recursive()) { if (AstNode* const impurep = m_statep->checkImpure(nodep)) { if (!isIfaceLocalImpure(nodep, impurep)) { - nodep->v3warn( - IMPURE, - "Unsupported: External variable referenced by non-inlined function/task: " - << nodep->prettyNameQ() << '\n' - << nodep->warnContextPrimary() << '\n' - << impurep->warnOther() << "... Location of the external reference: " - << impurep->prettyNameQ() << '\n' - << impurep->warnContextSecondary()); + nodep->v3warn(IMPURE, "Unsupported: External variable referenced by " + "non-inlined function/task: " + << nodep->prettyNameQ() << '\n' + << nodep->warnContextPrimary() << '\n' + << impurep->warnOther() + << "... Location of the external reference: " + << impurep->prettyNameQ() << '\n' + << impurep->warnContextSecondary()); } } } diff --git a/test_regress/t/t_interface_func_impure_bad.py b/test_regress/t/t_interface_func_impure_bad.py old mode 100644 new mode 100755 From 5d1b4fe8a88a0c1141499e4580d3c4d6fb931fa6 Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Tue, 28 Apr 2026 03:15:25 +0200 Subject: [PATCH 009/350] Fix generic interface port forwarded to a nested instance (#7454) (#7457) Fixes #7454. --- src/V3LinkDot.cpp | 31 +++++++++++++- src/V3Param.cpp | 38 ++++++++++++++++++ .../t/t_interface_generic_iface_forward.py | 18 +++++++++ .../t/t_interface_generic_iface_forward.v | 40 +++++++++++++++++++ 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100755 test_regress/t/t_interface_generic_iface_forward.py create mode 100644 test_regress/t/t_interface_generic_iface_forward.v diff --git a/src/V3LinkDot.cpp b/src/V3LinkDot.cpp index 148e639ab..47401a68b 100644 --- a/src/V3LinkDot.cpp +++ b/src/V3LinkDot.cpp @@ -3609,7 +3609,36 @@ class LinkDotResolveVisitor final : public VNVisitor { while (const AstNodePreSel* const preSelp = VN_CAST(exprp, NodePreSel)) { exprp = preSelp->fromp(); } - if (const AstVarRef* const varRefp = VN_CAST(exprp, VarRef)) { + // Resolve pin expression to the enclosing module's port Var. At primary + // LinkDot the expression may still be an AstParseRef, so also look up by name. + AstVar* enclosingVarp = nullptr; + const AstVarRef* const varRefp = VN_CAST(exprp, VarRef); + if (varRefp) { + enclosingVarp = varRefp->varp(); + } else if (const AstParseRef* const parseRefp = VN_CAST(exprp, ParseRef)) { + if (m_modp) { + if (VSymEnt* const symp + = m_statep->getNodeSym(m_modp)->findIdFlat(parseRefp->name())) { + enclosingVarp = VN_CAST(symp->nodep(), Var); + } + } + } + if (enclosingVarp && enclosingVarp->varType() == VVarType::IFACEREF + && VN_IS(enclosingVarp->childDTypep()->skipRefp(), IfaceGenericDType)) { + // Nested generic-iface forwarding (#7454): enclosing port is itself still + // generic, so emit a placeholder __VGIfaceParam pin carrying a VarRef to + // the outer port. V3Param rewrites it to the concrete IfaceRefDType once + // the enclosing module is specialized (see + // ParamProcessor::resolveGenericIfaceForwardingPins for the ordering + // constraint that keeps the rewrite inside V3Param). + AstVarRef* const fwdRefp + = new AstVarRef{exprp->fileline(), enclosingVarp, VAccess::READ}; + AstPin* const newPinp = new AstPin{ + pinp->fileline(), paramNum, "__VGIfaceParam" + modIfaceVarp->name(), fwdRefp}; + newPinp->param(true); + visit(newPinp); + nodep->addParamsp(newPinp); + } else if (varRefp) { const AstVar* const varp = varRefp->varp(); if (const AstIfaceRefDType* const refp = VN_CAST(getElemDTypep(varp->childDTypep()), IfaceRefDType)) { diff --git a/src/V3Param.cpp b/src/V3Param.cpp index 480432dae..13ead637a 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -1360,6 +1360,40 @@ class ParamProcessor final { return isEq.isNeqZero(); } + // Rewrite placeholder __VGIfaceParam pins emitted by + // LinkDotResolveVisitor::addImplicitParametersOfGenericIface for a nested + // generic-interface forwarding chain (#7454). Must run here (not in + // V3LinkDot's Param/Resolve visitor at LDS_PARAMED) because: + // * linkDotParamed runs AFTER V3Param, at which point cellDeparam has + // already consumed the pin via moduleFindOrClone (variant naming off + // pinp->exprp()'s IfaceRefDType) and genericInterfaceVarSetup (hard + // VN_AS cast to IfaceRefDType); and + // * the enclosing module's port only becomes a concrete IfaceRefDType + // after its own nodeDeparam has specialized it. That specialization + // happens in V3Param's top-down walk, so the window between "outer + // port resolved" and "inner cellDeparam starts" exists only here. + void resolveGenericIfaceForwardingPins(AstPin* paramsp) { + for (AstPin* pinp = paramsp; pinp; pinp = VN_AS(pinp->nextp(), Pin)) { + if (!pinp->exprp()) continue; + if (!VString::startsWith(pinp->name(), "__VGIfaceParam")) continue; + const AstNodeVarRef* const fwdRefp = VN_CAST(pinp->exprp(), NodeVarRef); + if (!fwdRefp) continue; + const AstIfaceRefDType* const resolvedp + = VN_CAST(fwdRefp->varp()->childDTypep()->skipRefp(), IfaceRefDType); + UASSERT_OBJ(resolvedp, pinp, + "Generic interface forwarding pin not specialized before use"); + AstIfaceRefDType* const newIrefp = new AstIfaceRefDType{ + resolvedp->fileline(), resolvedp->modportFileline(), resolvedp->cellName(), + resolvedp->ifaceName(), resolvedp->modportName()}; + newIrefp->ifacep(resolvedp->ifacep()); + if (resolvedp->paramsp()) { + newIrefp->addParamsp(resolvedp->paramsp()->cloneTree(true)); + } + pinp->exprp()->unlinkFrBack()->deleteTree(); + pinp->exprp(newIrefp); + } + } + void cellPinCleanup(AstNode* nodep, AstPin* pinp, AstPin* paramsp, AstNodeModule* srcModp, string& longnamer, bool& any_overridesr) { if (!pinp->exprp()) return; // No-connect @@ -2114,6 +2148,10 @@ public: } pinp = nextp; } + // Nested generic-iface forwarding (#7454): rewrite VarRef placeholders + // left by V3LinkDot to concrete IfaceRefDTypes now that the enclosing + // module has been specialized. + resolveGenericIfaceForwardingPins(cellp->paramsp()); } // Create new module name with _'s between the constants UINFOTREE(10, nodep, "", "cell"); diff --git a/test_regress/t/t_interface_generic_iface_forward.py b/test_regress/t/t_interface_generic_iface_forward.py new file mode 100755 index 000000000..6ac2815da --- /dev/null +++ b/test_regress/t/t_interface_generic_iface_forward.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=["--timing"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_interface_generic_iface_forward.v b/test_regress/t/t_interface_generic_iface_forward.v new file mode 100644 index 000000000..2608a3ac9 --- /dev/null +++ b/test_regress/t/t_interface_generic_iface_forward.v @@ -0,0 +1,40 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +// verilog_format: on + +interface inf #(parameter int PARAM = 1); + logic [PARAM-1:0] v; +endinterface + +module leaf (interface d); + initial begin + #1; + `checkd(d.v, 13); + `checkd(d.PARAM, 5); + end +endmodule + +module mid2 (interface c); + leaf leaf_i(.d(c)); +endmodule + +module mid1 (interface b); + mid2 mid2_i(.c(b)); +endmodule + +module t; + inf #(.PARAM(5)) a(); + mid1 mid1_i(.b(a)); + initial begin + a.v = 13; + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule From 327fc4ffbea98834223b4bcee574176c879f7d06 Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Tue, 28 Apr 2026 12:10:53 +0200 Subject: [PATCH 010/350] Support randomize() with (identifier_list) {constraint_block} (#7486) (#7507) Fixes #7486. --- src/V3AstNodeExpr.h | 24 ++ src/V3AstNodes.cpp | 27 ++ src/V3LinkDot.cpp | 105 ++++++-- src/verilog.y | 27 +- test_regress/t/t_debug_emitv.out | 236 +++++++++++++++++- test_regress/t/t_debug_emitv.v | 6 + .../t/t_randomize_with_constraint.out | 8 - test_regress/t/t_randomize_with_constraint.py | 9 +- test_regress/t/t_randomize_with_constraint.v | 94 ++++++- .../t/t_randomize_with_idlist_bad.out | 10 + test_regress/t/t_randomize_with_idlist_bad.py | 16 ++ test_regress/t/t_randomize_with_idlist_bad.v | 30 +++ .../t/t_randomize_with_idlist_method_bad.out | 5 + .../t/t_randomize_with_idlist_method_bad.py | 16 ++ .../t/t_randomize_with_idlist_method_bad.v | 15 ++ 15 files changed, 580 insertions(+), 48 deletions(-) delete mode 100644 test_regress/t/t_randomize_with_constraint.out create mode 100644 test_regress/t/t_randomize_with_idlist_bad.out create mode 100755 test_regress/t/t_randomize_with_idlist_bad.py create mode 100644 test_regress/t/t_randomize_with_idlist_bad.v create mode 100644 test_regress/t/t_randomize_with_idlist_method_bad.out create mode 100755 test_regress/t/t_randomize_with_idlist_method_bad.py create mode 100644 test_regress/t/t_randomize_with_idlist_method_bad.v diff --git a/src/V3AstNodeExpr.h b/src/V3AstNodeExpr.h index 56d302730..613bcc004 100644 --- a/src/V3AstNodeExpr.h +++ b/src/V3AstNodeExpr.h @@ -537,6 +537,12 @@ class AstWith final : public AstNode { // @astgen op2 := valueArgRefp : AstLambdaArgRef // @astgen op3 := exprp : List[AstNode] // Pins, expression and constraints // TODO: Separate expression and constraints +private: + // 'with (identifier_list) {...}' restricted form (IEEE 1800-2023 18.7). + bool m_restricted = false; + bool m_validated = false; // identifier_list typo / unused checks already run + std::set m_restrictedNames; + public: AstWith(FileLine* fl, AstLambdaArgRef* indexArgRefp, AstLambdaArgRef* valueArgRefp, AstNode* exprp) @@ -553,6 +559,18 @@ public: BROKEN_RTN(!valueArgRefp()); // varp needed to know lambda's arg dtype return nullptr; } + void dump(std::ostream& str) const override; + void dumpJson(std::ostream& str) const override; + void restricted(bool flag) { m_restricted = flag; } + bool restricted() const { return m_restricted; } + bool validated() const { return m_validated; } + void validated(bool flag) { m_validated = flag; } + void addRestrictedName(const std::string& name) { m_restrictedNames.insert(name); } + const std::set& restrictedNames() const { return m_restrictedNames; } + // True if 'name' binds into the randomize() target class. + bool nameResolvesToTarget(const std::string& name) const { + return !m_restricted || m_restrictedNames.count(name); + } }; // === AstNodeExpr === @@ -2821,6 +2839,10 @@ class AstWithParse final : public AstNodeExpr { // @astgen op1 := funcrefp : AstNodeExpr // @astgen op3 := exprsp : List[AstNodeExpr] // With's parenthesis part // @astgen op4 := constraintsp : List[AstNode] // With's braces part +private: + // True for the 'with (identifier_list) {...}' form, including 'with () {...}'. + bool m_restricted = false; + public: AstWithParse(FileLine* fl, AstNodeExpr* funcrefp, AstNodeExpr* exprsp, AstNode* constraintsp = nullptr) @@ -2831,6 +2853,8 @@ public: } ASTGEN_MEMBERS_AstWithParse; bool sameNode(const AstNode* /*samep*/) const override { return true; } + bool restricted() const { return m_restricted; } + void restricted(bool flag) { m_restricted = flag; } string emitVerilog() override { V3ERROR_NA_RETURN(""); } string emitC() override { V3ERROR_NA_RETURN(""); } diff --git a/src/V3AstNodes.cpp b/src/V3AstNodes.cpp index 9745f65b8..03696cb55 100644 --- a/src/V3AstNodes.cpp +++ b/src/V3AstNodes.cpp @@ -3558,3 +3558,30 @@ const char* AstNot::widthMismatch() const VL_MT_STABLE { BROKEN_RTN(lhsp()->widthMin() != widthMin()); return nullptr; } +void AstWith::dump(std::ostream& str) const { + this->AstNode::dump(str); + if (m_restricted) { + str << " [RESTRICTED={"; + bool first = true; + for (const std::string& n : m_restrictedNames) { + if (!first) str << ","; + str << n; + first = false; + } + str << "}]"; + } +} +void AstWith::dumpJson(std::ostream& str) const { + dumpJsonBoolIf(str, "restricted", m_restricted); + if (m_restricted) { + std::string joined; + bool first = true; + for (const std::string& n : m_restrictedNames) { + if (!first) joined += ","; + joined += n; + first = false; + } + dumpJsonStr(str, "restrictedNames", joined); + } + dumpJsonGen(str); +} diff --git a/src/V3LinkDot.cpp b/src/V3LinkDot.cpp index 47401a68b..758352a7f 100644 --- a/src/V3LinkDot.cpp +++ b/src/V3LinkDot.cpp @@ -2194,24 +2194,39 @@ class LinkDotFindVisitor final : public VNVisitor { } } // Type depends on the method used, let V3Width figure it out later - if (nodep->exprsp() - || nodep->constraintsp()) { // Else empty expression and pretend no "with" - AstNode* exprOrConstraintsp = nullptr; - if (nodep->exprsp() && nodep->constraintsp()) { - // When support this probably should change AstWith to separate out - // the expr from the constraint equation using separate op2/op3 similar - // to AstWithParse - nodep->v3warn(E_UNSUPPORTED, "Unsupported: 'randomize with (...) {...}'"); - } else if (nodep->exprsp()) - exprOrConstraintsp = nodep->exprsp()->unlinkFrBackWithNext(); - if (nodep->constraintsp()) - exprOrConstraintsp = AstNode::addNext( - exprOrConstraintsp, nodep->constraintsp()->unlinkFrBackWithNext()); + // 'with (...) { ... }' constraint_block form is randomize-only + // (IEEE 1800-2023 7.12 vs. 18.7); array methods take 'with (expr)'. + if (nodep->restricted() && funcrefp->name() != "randomize") { + nodep->v3error("'with (...) { ... }' constraint block is only valid for" + " randomize() (IEEE 1800-2023 18.7)"); + funcrefp->addArgsp(argsp); + nodep->replaceWith(nodep->funcrefp()->unlinkFrBack()); + VL_DO_DANGLING(nodep->deleteTree(), nodep); + return; + } + const bool restrictedRandomize = nodep->restricted(); + if (nodep->exprsp() || nodep->constraintsp() + || restrictedRandomize) { // Else empty expression and pretend no "with" AstLambdaArgRef* const indexArgRefp = new AstLambdaArgRef{argFl, name + "__DOT__index", true}; AstLambdaArgRef* const valueArgRefp = new AstLambdaArgRef{argFl, name, false}; AstWith* const newp - = new AstWith{nodep->fileline(), indexArgRefp, valueArgRefp, exprOrConstraintsp}; + = new AstWith{nodep->fileline(), indexArgRefp, valueArgRefp, nullptr}; + // Harvest the identifier_list into AstWith; grammar guarantees AstParseRef. + if (restrictedRandomize) { + newp->restricted(true); + while (AstNode* const itemp = nodep->exprsp()) { + newp->addRestrictedName(VN_AS(itemp, ParseRef)->name()); + itemp->unlinkFrBack(); + VL_DO_DANGLING(pushDeletep(itemp), itemp); + } + } + AstNode* exprOrConstraintsp = nullptr; + if (nodep->exprsp()) exprOrConstraintsp = nodep->exprsp()->unlinkFrBackWithNext(); + if (nodep->constraintsp()) + exprOrConstraintsp = AstNode::addNext( + exprOrConstraintsp, nodep->constraintsp()->unlinkFrBackWithNext()); + newp->addExprp(exprOrConstraintsp); // addExprp() tolerates nullptr funcrefp->withp(newp); } funcrefp->addArgsp(argsp); @@ -3082,7 +3097,9 @@ class LinkDotResolveVisitor final : public VNVisitor { int m_modportNum = 0; // Uniqueify modport numbers int m_indent = 0; // Indentation (tree depth) for debug bool m_inSens = false; // True if in senitem - bool m_inWith = false; // True if in with + const AstWith* m_currentWithp = nullptr; // Enclosing 'with' (nullptr if none) + std::set + m_restrictedNamesUsed; // Names from current 'with (id_list)' that resolved into target bool m_genericIfaceModule = false; // True if in module containing generic interface std::map m_ifClassImpNames; // Names imported from interface class std::set m_extendsParam; // Classes that have a parameterized super class @@ -3911,7 +3928,7 @@ class LinkDotResolveVisitor final : public VNVisitor { VSymEnt* classSymp = getThisClassSymp(); // In 'randomize() with { this.member }', 'this' refers to randomized // object, not the calling class (IEEE 1800-2023 18.7) - if (m_randSymp && m_inWith) classSymp = m_randSymp; + if (m_randSymp && m_currentWithp) classSymp = m_randSymp; if (!classSymp) { nodep->v3error("'this' used outside class (IEEE 1800-2023 8.11)"); m_ds.m_dotErr = true; @@ -4224,11 +4241,16 @@ class LinkDotResolveVisitor final : public VNVisitor { VSymEnt* foundp; string baddot; VSymEnt* okSymp = nullptr; - if (m_randSymp) { + // Restricted 'with (id_list)': only id_list names bind into target class. + if (m_randSymp + && (!m_currentWithp || m_currentWithp->nameResolvesToTarget(nodep->name()))) { foundp = m_randSymp->findIdFlat(nodep->name()); if (foundp) { + if (m_currentWithp && m_currentWithp->restricted()) { + m_restrictedNamesUsed.insert(nodep->name()); + } if (!start) m_ds.m_dotPos = DP_MEMBER; - if (!m_inWith) { + if (!m_currentWithp) { UASSERT_OBJ(m_randMethodCallp, nodep, "Expected to be under randomize()"); // This will start failing once complex expressions are allowed on the LHS // of randomize() with args @@ -5174,9 +5196,14 @@ class LinkDotResolveVisitor final : public VNVisitor { dotSymp = m_statep->findDotted(nodep->fileline(), dotSymp, nodep->dotted(), baddot, okSymp, true); // Maybe nullptr } - if (m_randSymp) { + // Restricted 'with (id_list)': only id_list names bind into target class. + if (m_randSymp + && (!m_currentWithp || m_currentWithp->nameResolvesToTarget(nodep->name()))) { VSymEnt* const foundp = m_randSymp->findIdFlat(nodep->name()); - if (foundp && m_inWith) { + if (foundp && m_currentWithp) { + if (m_currentWithp->restricted()) { + m_restrictedNamesUsed.insert(nodep->name()); + } UINFO(9, indent() << "randomize-with fromSym " << foundp->nodep()); AstArg* argsp = nullptr; if (nodep->argsp()) { @@ -5569,11 +5596,45 @@ class LinkDotResolveVisitor final : public VNVisitor { UINFO(5, indent() << "visit " << nodep); checkNoDot(nodep); VL_RESTORER(m_curSymp); - VL_RESTORER(m_inWith); + VL_RESTORER(m_currentWithp); + VL_RESTORER(m_restrictedNamesUsed); { m_ds.m_dotSymp = m_curSymp = m_statep->getNodeSym(nodep); - m_inWith = true; + m_currentWithp = nodep; + // Validate the identifier_list once per AstWith (m_validated travels + // with cloneTree, so cloned witnesses skip the redundant check). + const bool firstVisit = !nodep->validated(); + if (firstVisit) { + nodep->validated(true); + m_restrictedNamesUsed.clear(); + // IEEE 1800-2023 18.7: each name in the identifier_list must designate + // a member of the randomize() target class. + if (nodep->restricted() && m_randSymp) { + for (const std::string& n : nodep->restrictedNames()) { + if (!m_randSymp->findIdFlat(n)) { + nodep->v3error("Identifier '" + << n + << "' in 'with (...)' identifier list is not a" + " member of the randomize() target class" + " (IEEE 1800-2023 18.7)"); + m_restrictedNamesUsed.insert(n); + } + } + } + } iterateChildren(nodep); + // Flag names that the user listed but never referenced in the constraint. + if (firstVisit && nodep->restricted()) { + for (const std::string& n : nodep->restrictedNames()) { + if (!m_restrictedNamesUsed.count(n)) { + nodep->v3warn(UNUSED, "Identifier '" + << n + << "' in 'with (...)' identifier list is" + " not referenced in the constraint" + " block"); + } + } + } } m_ds.m_dotSymp = VL_RESTORER_PREV(m_curSymp); } diff --git a/src/verilog.y b/src/verilog.y index d1923d820..f13d40a8c 100644 --- a/src/verilog.y +++ b/src/verilog.y @@ -4194,7 +4194,11 @@ task_subroutine_callNoMethod: // function_subroutine_callNoMethod // // We implement randomize as a normal funcRef, since randomize isn't a keyword // // Note yNULL is already part of expressions, so they come for free | funcRef yWITH__CUR constraint_block { $$ = new AstWithParse{$2, $1, nullptr, $3}; } - | funcRef yWITH__PAREN_CUR '(' expr ')' constraint_block { $$ = new AstWithParse{$2, $1, $4, $6}; } + | funcRef yWITH__PAREN_CUR '(' inlineConstraintIdListE ')' constraint_block + { AstWithParse* const withParsep + = new AstWithParse{$2, $1, $4, $6}; + withParsep->restricted(true); + $$ = withParsep; } ; function_subroutine_callNoMethod: // IEEE: function_subroutine_call (as function) @@ -4211,7 +4215,11 @@ function_subroutine_callNoMethod: // IEEE: function_subroutine // // We implement randomize as a normal funcRef, since randomize isn't a keyword // // Note yNULL is already part of expressions, so they come for free | funcRef yWITH__CUR constraint_block { $$ = new AstWithParse{$2, $1, nullptr, $3}; } - | funcRef yWITH__PAREN_CUR '(' expr ')' constraint_block { $$ = new AstWithParse{$2, $1, $4, $6}; } + | funcRef yWITH__PAREN_CUR '(' inlineConstraintIdListE ')' constraint_block + { AstWithParse* const withParsep + = new AstWithParse{$2, $1, $4, $6}; + withParsep->restricted(true); + $$ = withParsep; } ; system_t_stmt_call: // IEEE: part of system_tf_call (as task returning statement) @@ -5407,6 +5415,21 @@ exprList: | exprList ',' expr { $$ = $1->addNext($3); } ; +// identifier_list for 'with' in inline randomize constraints (IEEE 1800-2023 18.7). +// Only simple identifiers; non-identifier expressions are parse errors. +inlineConstraintIdList: + id { $$ = new AstParseRef{$1, *$1, nullptr, nullptr}; } + | inlineConstraintIdList ',' id { $$ = $1->addNext( + new AstParseRef{$3, *$3, nullptr, nullptr}); } + ; + +// Optional identifier_list. Empty 'with () {...}' differs from bare 'with {...}' +// via AstWithParse::restricted(). +inlineConstraintIdListE: + /* empty */ { $$ = nullptr; } + | inlineConstraintIdList { $$ = $1; } + ; + exprEListE: // expression list with empty commas allowed exprE { $$ = $1; } | exprEListE ',' exprE { $$ = addNextNull($1, $3); } diff --git a/test_regress/t/t_debug_emitv.out b/test_regress/t/t_debug_emitv.out index 005b185bc..ec14367d9 100644 --- a/test_regress/t/t_debug_emitv.out +++ b/test_regress/t/t_debug_emitv.out @@ -682,10 +682,224 @@ module Vt_debug_emitv_t; restrict (@(posedge clk) ##1 a[0] ); endmodule +package Vt_debug_emitv_std; + class Vt_debug_emitv_semaphore; + int signed m_keyCount; + function new; + input int signed keyCount; + m_keyCount = keyCount; + endfunction + task put; + input int signed keyCount; + m_keyCount = (m_keyCount + keyCount); + endtask + task get; + input int signed keyCount; + while ((m_keyCount < keyCount)) begin + begin + + ???? // WAIT + (m_keyCount >= keyCount)end + end + m_keyCount = (m_keyCount - keyCount); + endtask + function try_get; + input int signed keyCount; + begin : label3 + try_get = /*CRESET*/; + if ((m_keyCount >= keyCount)) begin + begin + m_keyCount = (m_keyCount - + keyCount); + try_get = 'sh1; + disable label3; + end + end + try_get = 'sh0; + disable label3; + end + endfunction + endclass + class Vt_debug_emitv_process; + typedef enum int signed{ + FINISHED = 32'h0, + RUNNING = 32'h1, + WAITING = 32'h2, + SUSPENDED = 32'h3, + KILLED = 32'h4 + } state; + VlProcessRef m_process; + function self; + + ???? // CLASSREFDTYPE 'process' + p + ???? // CLASSREFDTYPE 'process' + ; + begin : label4 + self = /*CRESET*/; + p = new(); + $c(p.m_process = vlProcess;); + self = p; + disable label4; + end + endfunction + task set_status; + input int signed s; + $c(m_process->state(s);); + endtask + function status; + begin : label5 + status = /*CRESET*/; + status = ($c(m_process->state())); + disable label5; + end + endfunction + task kill; + set_status(process::KILLED); + endtask + task suspend; + $error("std::process::suspend() not supported"); + $stop; + endtask + task resume; + set_status(process::RUNNING); + endtask + task await; + + ???? // WAIT + ((status() == process::FINISHED) || (status() + == + process:: + KILLED))endtask + task killQueue; + ref + ???? // CLASSREFDTYPE 'process' + processQueue[$] + ???? // CLASSREFDTYPE 'process' + ; + begin : unnamedblk1_1 + integer signed __Vrepeat0; + __Vrepeat0 = processQueue.size(); + while ((__Vrepeat0 > 32'h0)) begin + begin + kill(); + end + __Vrepeat0 = (__Vrepeat0 - 32'h1); + end + end + endtask + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + + ???? // SYSTEMCSECTION + function get_randstate; + string s; + begin : label6 + get_randstate = /*CRESET*/; + s = string'($c(0)); + $c(s = m_process->randstate();); + get_randstate = s; + disable label6; + end + endfunction + task set_randstate; + input string s; + $c(m_process->randstate(s);); + endtask + function new; + endfunction + process::FINISHEDprocess::KILLEDprocess::RUNNINGprocess::SUSPENDEDprocess::WAITINGendclass + function randomize; + randomize = 'sh0; + endfunction + typedef struct { + string name; + int signed weight; + int signed goal; + string comment; + int signed at_least; + int signed auto_bin_max; + int signed cross_num_print_missing; + bit cross_retain_auto_bins; + bit detect_overlap; + bit per_instance; + bit get_inst_coverage; + } vl_covergroup_options_t; + typedef struct { + int signed weight; + int signed goal; + string comment; + int signed at_least; + int signed auto_bin_max; + bit detect_overlap; + } vl_coverpoint_options_t; + typedef struct { + int signed weight; + int signed goal; + string comment; + int signed at_least; + int signed cross_num_print_missing; + bit cross_retain_auto_bins; + } vl_cross_options_t; + typedef struct { + int signed weight; + int signed goal; + string comment; + bit strobe; + bit merge_instances; + bit distribute_first; + real real_interval; + } vl_covergroup_type_options_t; + typedef struct { + int signed weight; + int signed goal; + string comment; + real real_interval; + } vl_coverpoint_type_options_t; + typedef struct { + int signed weight; + int signed goal; + string comment; + } vl_cross_type_options_t; +endpackage package Vt_debug_emitv___024unit; class Vt_debug_emitv_Cls; int signed member; member = 'sh1; + int signed rmember1; + int signed rmember2; task method; if ((this != this)) begin $stop; @@ -693,7 +907,23 @@ package Vt_debug_emitv___024unit; endtask function new; endfunction + function randomize; + endfunction endclass + function rand_restricted; + input + ???? // CLASSREFDTYPE 'Cls' + obj + ???? // CLASSREFDTYPE 'Cls' + ; + input int signed member; + begin : label7 + rand_restricted = randomize() with ( + ???? // CONSTRAINTEXPR + (item.rmember1 < member)) ; + disable label7; + end + endfunction endpackage interface Vt_debug_emitv_Iface; input logic clk; @@ -712,13 +942,13 @@ module Vt_debug_emitv_sub; endtask function f; input int signed v; - begin : label3 + begin : label8 if ((v == 'sh0)) begin f = 'sh21; - disable label3; + disable label8; end f = ({32'h1{{31'h0, v[2]}}} + 32'h1); - disable label3; + disable label8; end endfunction real r; diff --git a/test_regress/t/t_debug_emitv.v b/test_regress/t/t_debug_emitv.v index 4f94bd864..1bb4ebed8 100644 --- a/test_regress/t/t_debug_emitv.v +++ b/test_regress/t/t_debug_emitv.v @@ -21,11 +21,17 @@ endpackage class Cls; int member = 1; + rand int rmember1; + rand int rmember2; function void method; if (this != this) $stop; endfunction endclass +function int rand_restricted(Cls obj, int member); + return obj.randomize() with (rmember1, rmember2) { rmember1 < member; rmember2 < member; }; +endfunction + interface Iface ( input clk ); diff --git a/test_regress/t/t_randomize_with_constraint.out b/test_regress/t/t_randomize_with_constraint.out deleted file mode 100644 index b784f8905..000000000 --- a/test_regress/t/t_randomize_with_constraint.out +++ /dev/null @@ -1,8 +0,0 @@ -%Error-UNSUPPORTED: t/t_randomize_with_constraint.v:13:26: Unsupported: 'randomize with (...) {...}' - 13 | return obj.randomize() with ( - | ^~~~ - ... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest -%Error-UNSUPPORTED: t/t_randomize_with_constraint.v:21:26: Unsupported: 'randomize with (...) {...}' - 21 | return obj.randomize() with ( - | ^~~~ -%Error: Exiting due to diff --git a/test_regress/t/t_randomize_with_constraint.py b/test_regress/t/t_randomize_with_constraint.py index f3bbcad9d..8862c2c31 100755 --- a/test_regress/t/t_randomize_with_constraint.py +++ b/test_regress/t/t_randomize_with_constraint.py @@ -9,8 +9,13 @@ import vltest_bootstrap -test.scenarios('vlt') +test.scenarios('simulator') -test.lint(fails=True, expect_filename=test.golden_filename) +if not test.have_solver: + test.skip("No constraint solver installed") + +test.compile() + +test.execute() test.passes() diff --git a/test_regress/t/t_randomize_with_constraint.v b/test_regress/t/t_randomize_with_constraint.v index 496a31522..8fcde8101 100644 --- a/test_regress/t/t_randomize_with_constraint.v +++ b/test_regress/t/t_randomize_with_constraint.v @@ -4,33 +4,105 @@ // SPDX-FileCopyrightText: 2025 Wilson Snyder // SPDX-License-Identifier: CC0-1.0 +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +`define check_range(gotv,minv,maxv) do if ((gotv) < (minv) || (gotv) > (maxv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d-%0d\n", `__FILE__,`__LINE__, (gotv), (minv), (maxv)); `stop; end while(0); +// verilog_format: on + class Cls; rand int m_x; - int m_y = -1; + rand int m_z; + int y = -1; // class member named 'y' intentionally shadows the caller arg + int lo = -100; // class member named 'lo' intentionally shadows the caller arg endclass -function int func1(Cls obj, int y); - return obj.randomize() with ( - m_x) { +// 'y' is not in the list -> resolves to the caller arg (10), not class member (-1). +function int func_restricted(Cls obj, int y); + return obj.randomize() with (m_x) { m_x > 0; m_x < y; }; endfunction -function int func2(Cls obj, int y); - return obj.randomize() with ( - m_x) { - m_x > 0; - m_x < m_y; +// Multi-id list: 'lo' is also a class member but resolves to caller arg. +function int func_multi(Cls obj, int lo, int hi); + return obj.randomize() with (m_x, m_z) { + m_x > lo; + m_x < hi; + m_z > lo; + m_z < hi; + m_x < m_z; }; endfunction +// Unrestricted baseline: no id list. +function int func_unrestricted(Cls obj); + return obj.randomize() with { + m_x > 0; + m_x < 10; + }; +endfunction + +// Empty id list: no name binds into the class; constraint references obj.* explicitly. +function automatic int func_empty(Cls obj, int lo, int hi); + // verilog_format: off + return obj.randomize() with () { + obj.m_x > lo; + obj.m_x < hi; + }; + // verilog_format: on +endfunction + +// 'local::y' bypasses class scope -> caller arg, not obj.y. +function int func_local_qual(Cls obj, int y); + obj.y = -42; + // verilog_format: off + return obj.randomize() with { + m_x > 0; + m_x < local::y; + }; + // verilog_format: on +endfunction + +// Consecutive restricted blocks must not leak each other's id list. +function automatic int func_sequential(Cls obj, int hi); + int rc = 1; + rc &= obj.randomize() with (m_x) { m_x > 0; m_x < hi; }; + rc &= obj.randomize() with (m_z) { m_z > 0; m_z < hi; }; + return rc; +endfunction + module t; initial begin Cls c; int i; c = new; - i = func1(c, 2); - i = func2(c, 2); + repeat (20) begin + i = func_restricted(c, 10); + `checkd(i, 1); + `check_range(c.m_x, 1, 9); + i = func_multi(c, 0, 50); + `checkd(i, 1); + `check_range(c.m_x, 1, 49); + `check_range(c.m_z, 1, 49); + `checkd(c.m_x < c.m_z, 1); + i = func_unrestricted(c); + `checkd(i, 1); + `check_range(c.m_x, 1, 9); + i = func_empty(c, 0, 9); + `checkd(i, 1); + `check_range(c.m_x, 1, 8); + i = func_local_qual(c, 8); + `checkd(i, 1); + `check_range(c.m_x, 1, 7); + i = func_sequential(c, 6); + `checkd(i, 1); + // Statement form: discards return value via void'. + void'(c.randomize() with (m_x) { m_x > 0; m_x < 5; }); + `check_range(c.m_x, 1, 4); + end + $write("*-* All Finished *-*\n"); + $finish; end endmodule diff --git a/test_regress/t/t_randomize_with_idlist_bad.out b/test_regress/t/t_randomize_with_idlist_bad.out new file mode 100644 index 000000000..07e204fd4 --- /dev/null +++ b/test_regress/t/t_randomize_with_idlist_bad.out @@ -0,0 +1,10 @@ +%Error: t/t_randomize_with_idlist_bad.v:14:26: Identifier 'm_z' in 'with (...)' identifier list is not a member of the randomize() target class (IEEE 1800-2023 18.7) + 14 | return obj.randomize() with (m_x, m_z) { m_x > 0; }; + | ^~~~ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Warning-UNUSED: t/t_randomize_with_idlist_bad.v:19:26: Identifier 'm_y' in 'with (...)' identifier list is not referenced in the constraint block + 19 | return obj.randomize() with (m_x, m_y) { m_x > 0; }; + | ^~~~ + ... For warning description see https://verilator.org/warn/UNUSED?v=latest + ... Use "/* verilator lint_off UNUSED */" and lint_on around source to disable this message. +%Error: Exiting due to diff --git a/test_regress/t/t_randomize_with_idlist_bad.py b/test_regress/t/t_randomize_with_idlist_bad.py new file mode 100755 index 000000000..344a4e20a --- /dev/null +++ b/test_regress/t/t_randomize_with_idlist_bad.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_randomize_with_idlist_bad.v b/test_regress/t/t_randomize_with_idlist_bad.v new file mode 100644 index 000000000..63eec40be --- /dev/null +++ b/test_regress/t/t_randomize_with_idlist_bad.v @@ -0,0 +1,30 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +class Cls; + rand int m_x; + rand int m_y; +endclass + +// 'm_z' is not a member of Cls -- typo (rejected as IEEE 1800-2023 18.7 violation). +function int F_unknown(Cls obj); + return obj.randomize() with (m_x, m_z) { m_x > 0; }; +endfunction + +// 'm_y' is a class member but never referenced in the constraint -- flagged unused. +function int F_unused(Cls obj); + return obj.randomize() with (m_x, m_y) { m_x > 0; }; +endfunction + +module t; + Cls c; + initial begin + c = new; + if (F_unknown(c) != 1) $stop; + if (F_unused(c) != 1) $stop; + $finish; + end +endmodule diff --git a/test_regress/t/t_randomize_with_idlist_method_bad.out b/test_regress/t/t_randomize_with_idlist_method_bad.out new file mode 100644 index 000000000..f77ddcb44 --- /dev/null +++ b/test_regress/t/t_randomize_with_idlist_method_bad.out @@ -0,0 +1,5 @@ +%Error: t/t_randomize_with_idlist_method_bad.v:13:21: 'with (...) { ... }' constraint block is only valid for randomize() (IEEE 1800-2023 18.7) + 13 | total = q.sum() with (item) { item > 0; }; + | ^~~~ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Error: Exiting due to diff --git a/test_regress/t/t_randomize_with_idlist_method_bad.py b/test_regress/t/t_randomize_with_idlist_method_bad.py new file mode 100755 index 000000000..344a4e20a --- /dev/null +++ b/test_regress/t/t_randomize_with_idlist_method_bad.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_randomize_with_idlist_method_bad.v b/test_regress/t/t_randomize_with_idlist_method_bad.v new file mode 100644 index 000000000..0484232da --- /dev/null +++ b/test_regress/t/t_randomize_with_idlist_method_bad.v @@ -0,0 +1,15 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +module t; + int q[$]; + int total; + initial begin + q.push_back(1); + q.push_back(2); + total = q.sum() with (item) { item > 0; }; + end +endmodule From bb1bfabab38680bffa3e6845327257bfae186c3d Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Tue, 28 Apr 2026 12:50:16 +0200 Subject: [PATCH 011/350] Fix internal error on multi-cycle SVA under default clocking (#7472) (#7506) --- src/V3AssertNfa.cpp | 20 +++ src/V3AssertPre.cpp | 37 ++--- .../t/t_property_default_clocking_sexpr.py | 18 +++ .../t/t_property_default_clocking_sexpr.v | 138 ++++++++++++++++++ 4 files changed, 191 insertions(+), 22 deletions(-) create mode 100755 test_regress/t/t_property_default_clocking_sexpr.py create mode 100644 test_regress/t/t_property_default_clocking_sexpr.v diff --git a/src/V3AssertNfa.cpp b/src/V3AssertNfa.cpp index bb388b846..b4bb45263 100644 --- a/src/V3AssertNfa.cpp +++ b/src/V3AssertNfa.cpp @@ -1467,6 +1467,8 @@ public: class AssertNfaVisitor final : public VNVisitor { // STATE AstNodeModule* m_modp = nullptr; // Current module being processed + AstClocking* m_defaultClockingp = nullptr; // Default clocking + AstDefaultDisable* m_defaultDisablep = nullptr; // Default disable iff SvaNfaLowering* m_loweringp = nullptr; // NFA-to-hardware lowering engine V3UniqueNames m_propVarNames{"__Vpropvar"}; // Property-local variable names V3UniqueNames m_disableCntNames{"__VnfaDis"}; // Disable-iff counter names @@ -1815,6 +1817,13 @@ class AssertNfaVisitor final : public VNVisitor { bool senTreeOwned = false; // True if we created senTreep locally AstPropSpec* const propSpecp = VN_CAST(assertp->propp(), PropSpec); UASSERT_OBJ(propSpecp, assertp, "Concurrent assertion must have PropSpec"); + // Inherit module defaults (IEEE 14.12, 16.15) when assertion has none. + if (!propSpecp->sensesp() && m_defaultClockingp) { + propSpecp->sensesp(m_defaultClockingp->sensesp()->cloneTree(true)); + } + if (!propSpecp->disablep() && m_defaultDisablep) { + propSpecp->disablep(m_defaultDisablep->condp()->cloneTreePure(true)); + } if (!senTreep && propSpecp->sensesp()) { senTreep = new AstSenTree{propSpecp->fileline(), propSpecp->sensesp()->cloneTree(true)}; @@ -1886,11 +1895,22 @@ class AssertNfaVisitor final : public VNVisitor { void visit(AstNodeModule* nodep) override { VL_RESTORER(m_modp); VL_RESTORER(m_loweringp); + VL_RESTORER(m_defaultClockingp); + VL_RESTORER(m_defaultDisablep); m_modp = nodep; + m_defaultClockingp = nullptr; + m_defaultDisablep = nullptr; SvaNfaLowering lowering{nodep}; m_loweringp = &lowering; iterateChildren(nodep); } + void visit(AstClocking* nodep) override { + if (nodep->isDefault() && !m_defaultClockingp) m_defaultClockingp = nodep; + iterateChildren(nodep); + } + void visit(AstDefaultDisable* nodep) override { + if (!m_defaultDisablep) m_defaultDisablep = nodep; + } void visit(AstAssert* nodep) override { processAssertion(nodep); } void visit(AstCover* nodep) override { processAssertion(nodep); } void visit(AstRestrict* nodep) override { diff --git a/src/V3AssertPre.cpp b/src/V3AssertPre.cpp index 160a6c63a..2577aace7 100644 --- a/src/V3AssertPre.cpp +++ b/src/V3AssertPre.cpp @@ -240,6 +240,15 @@ private: VL_RESTORER(m_clockingp); m_clockingp = nodep; UINFO(8, " CLOCKING" << nodep); + if (nodep->isDefault()) { + if (m_defaultClockingp) { + nodep->v3error("Only one default clocking block allowed per module" + " (IEEE 1800-2023 14.12)"); + } else { + m_defaultClockingp = nodep; + m_defaultClkEvtVarp = nodep->ensureEventp(); + } + } iterateChildren(nodep); if (nodep->eventp()) nodep->addNextHere(nodep->eventp()->unlinkFrBack()); VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep); @@ -1182,7 +1191,12 @@ private: } void visit(AstDefaultDisable* nodep) override { - // Done with these + if (m_defaultDisablep) { + nodep->v3error("Only one 'default disable iff' allowed per module" + " (IEEE 1800-2023 16.15)"); + } else { + m_defaultDisablep = nodep; + } VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep); } void visit(AstInferredDisable* nodep) override { @@ -1293,29 +1307,8 @@ private: VL_RESTORER(m_modp); m_defaultClockingp = nullptr; m_defaultClkEvtVarp = nullptr; - nodep->foreach([&](AstClocking* const clockingp) { - if (clockingp->isDefault()) { - if (m_defaultClockingp) { - clockingp->v3error("Only one default clocking block allowed per module" - " (IEEE 1800-2023 14.12)"); - } - m_defaultClockingp = clockingp; - } - }); m_defaultDisablep = nullptr; - nodep->foreach([&](AstDefaultDisable* const disablep) { - if (m_defaultDisablep) { - disablep->v3error("Only one 'default disable iff' allowed per module" - " (IEEE 1800-2023 16.15)"); - } - m_defaultDisablep = disablep; - }); m_modp = nodep; - // Pre-create and cache the clocking event var before iterating children. - // visit(AstClocking) will unlink the event from the clocking node and place it - // in the module tree, then delete the clocking. After that, ensureEventp() would - // create an orphaned var. Caching here avoids this. - m_defaultClkEvtVarp = m_defaultClockingp ? m_defaultClockingp->ensureEventp() : nullptr; iterateChildren(nodep); } void visit(AstProperty* nodep) override { diff --git a/test_regress/t/t_property_default_clocking_sexpr.py b/test_regress/t/t_property_default_clocking_sexpr.py new file mode 100755 index 000000000..35e44000c --- /dev/null +++ b/test_regress/t/t_property_default_clocking_sexpr.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(timing_loop=True, verilator_flags2=['--assert', '--timing']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_property_default_clocking_sexpr.v b/test_regress/t/t_property_default_clocking_sexpr.v new file mode 100644 index 000000000..b3d2d52e8 --- /dev/null +++ b/test_regress/t/t_property_default_clocking_sexpr.v @@ -0,0 +1,138 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkh(gotv, expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +`define checkd(gotv, expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +// verilog_format: on + +// Issue #7472 reproducer (default clocking + |=> + throughout). +module wsn ( + input clk, + input a, b, c, d +); + default clocking @(posedge clk); endclocking + int fail = 0; + assert property ( + a |=> b throughout (c ##1 d) + ) else fail <= fail + 1; +endmodule + +// Explicit @(edge) must override default clocking. +module clk_override ( + input clk_default, + input clk_explicit, + input a, b, c, d +); + default clocking @(posedge clk_default); endclocking + int default_fail = 0; + int explicit_fail = 0; + assert property (a |=> b throughout (c ##1 d)) + else default_fail <= default_fail + 1; + assert property (@(posedge clk_explicit) a |=> b throughout (c ##1 d)) + else explicit_fail <= explicit_fail + 1; +endmodule + +// Explicit `disable iff` must override default disable. +module dgate ( + input clk, + input rst, + input a, b, c, d +); + default clocking @(posedge clk); endclocking + default disable iff (rst); + int default_dis_fail = 0; + int explicit_dis_fail = 0; + assert property (a |=> b throughout (c ##1 d)) + else default_dis_fail <= default_dis_fail + 1; + assert property (disable iff (1'b0) a |=> b throughout (c ##1 d)) + else explicit_dis_fail <= explicit_dis_fail + 1; +endmodule + +// No defaults, explicit clock -- fix must not alter this path. +module nodef ( + input clk, + input a, b, c, d +); + int fail = 0; + assert property (@(posedge clk) a |=> b throughout (c ##1 d)) + else fail <= fail + 1; +endmodule + +module t ( + input clk +); + integer cyc = 0; + reg [63:0] crc = '0; + + wire a = crc[0]; + wire b = crc[4]; + wire c = crc[8]; + wire d = crc[12]; + + wire clk_alt = ~clk; + wire rst = (cyc < 10); + + default clocking @(posedge clk); endclocking + default disable iff (cyc < 10); + + int count_fail1 = 0; + int count_fail2 = 0; + int count_fail3 = 0; + int count_fail4 = 0; + + // Issue #7472 exact shape. + assert property (a |=> b throughout (c ##1 d)) + else count_fail1 <= count_fail1 + 1; + assert property (a |-> b throughout (c ##1 d)) + else count_fail2 <= count_fail2 + 1; + assert property (a |=> c ##1 d) + else count_fail3 <= count_fail3 + 1; + assert property (a |=> b throughout (c throughout (c ##1 d))) + else count_fail4 <= count_fail4 + 1; + + cover property (a throughout (c ##[1:2] d)); + cover property (a |=> b throughout (c ##1 d)); + + // Generate-scope: module foreach must reach inside generate blocks. + generate + if (1) begin : g + int gen_fail = 0; + assert property (a |=> b throughout (c ##1 d)) + else gen_fail <= gen_fail + 1; + end + endgenerate + + wsn u_wsn (.clk(clk), .a(a), .b(b), .c(c), .d(d)); + clk_override u_override (.clk_default(clk), .clk_explicit(clk_alt), + .a(a), .b(b), .c(c), .d(d)); + dgate u_dgate (.clk(clk), .rst(rst), .a(a), .b(b), .c(c), .d(d)); + nodef u_nodef (.clk(clk), .a(a), .b(b), .c(c), .d(d)); + + always_ff @(posedge clk) begin + cyc <= cyc + 1; + crc <= {crc[62:0], crc[63] ^ crc[2] ^ crc[0]}; + if (cyc == 0) begin + crc <= 64'h5aef0c8d_d70a4497; + end else if (cyc == 99) begin + `checkh(crc, 64'hc77bb9b3784ea091); + `checkd(count_fail1, 35); + `checkd(count_fail2, 36); + `checkd(count_fail3, 29); + `checkd(count_fail4, 35); + `checkd(u_wsn.fail, 39); + `checkd(u_override.default_fail, 39); + `checkd(u_override.explicit_fail, 39); + `checkd(u_dgate.default_dis_fail, 35); + `checkd(u_dgate.explicit_dis_fail, 39); + `checkd(u_nodef.fail, 39); + `checkd(g.gen_fail, 35); + $write("*-* All Finished *-*\n"); + $finish; + end + end +endmodule From 93c594e18a5d4d910f4922c2ac9ecf23f19d7673 Mon Sep 17 00:00:00 2001 From: Nikolai Kumar Date: Tue, 28 Apr 2026 14:25:37 -0500 Subject: [PATCH 012/350] Fix mailbox#(packed_struct) type mismatch with parameterized class (#7494) (#7495) --- docs/CONTRIBUTORS | 1 + src/V3AstNodeDType.h | 4 +- src/V3AstNodes.cpp | 15 +++++++ test_regress/t/t_mailbox_struct_param.py | 18 +++++++++ test_regress/t/t_mailbox_struct_param.v | 51 ++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 3 deletions(-) create mode 100755 test_regress/t/t_mailbox_struct_param.py create mode 100644 test_regress/t/t_mailbox_struct_param.v diff --git a/docs/CONTRIBUTORS b/docs/CONTRIBUTORS index 1dc86878e..f67c45ae2 100644 --- a/docs/CONTRIBUTORS +++ b/docs/CONTRIBUTORS @@ -200,6 +200,7 @@ Nathan Graybeal Nathan Kohagen Nathan Myers Nick Brereton +Nikolai Kumar Nikolay Puzanov Nolan Poe Oleh Maksymenko diff --git a/src/V3AstNodeDType.h b/src/V3AstNodeDType.h index 8265ffa8d..6b9b19bf6 100644 --- a/src/V3AstNodeDType.h +++ b/src/V3AstNodeDType.h @@ -275,9 +275,7 @@ public: int widthAlignBytes() const override; // (Slow) recurses - Width in bytes rounding up 1,2,4,8,12,... int widthTotalBytes() const override; - bool similarDTypeNode(const AstNodeDType* samep) const override { - return this == samep; // We don't compare members, require exact equivalence - } + bool similarDTypeNode(const AstNodeDType* samep) const override; string name() const override VL_MT_STABLE { return m_name; } void name(const string& flag) override { m_name = flag; } bool packed() const VL_MT_SAFE { return m_packed; } diff --git a/src/V3AstNodes.cpp b/src/V3AstNodes.cpp index 03696cb55..a5a4da2f5 100644 --- a/src/V3AstNodes.cpp +++ b/src/V3AstNodes.cpp @@ -2003,6 +2003,21 @@ bool AstClassRefDType::similarDTypeNode(const AstNodeDType* samep) const { } return !lp && !rp; } +bool AstNodeUOrStructDType::similarDTypeNode(const AstNodeDType* samep) const { + const AstNodeUOrStructDType* const sp = VN_DBG_AS(samep, NodeUOrStructDType); + if (m_packed != sp->m_packed) return false; + if (fileline()->tokenNum() != sp->fileline()->tokenNum()) return false; + const AstMemberDType* lp = membersp(); + const AstMemberDType* rp = sp->membersp(); + while (lp && rp) { + if (lp->name() != rp->name()) return false; + if (lp->width() != rp->width()) return false; + if (!lp->subDTypep()->similarDType(rp->subDTypep())) return false; + lp = VN_CAST(lp->nextp(), MemberDType); + rp = VN_CAST(rp->nextp(), MemberDType); + } + return !lp && !rp; +} void AstNodeCoverOrAssert::dump(std::ostream& str) const { this->AstNodeStmt::dump(str); str << " ["s + this->userType().ascii() + "]"; diff --git a/test_regress/t/t_mailbox_struct_param.py b/test_regress/t/t_mailbox_struct_param.py new file mode 100755 index 000000000..6fe7d000c --- /dev/null +++ b/test_regress/t/t_mailbox_struct_param.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=["--binary"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_mailbox_struct_param.v b/test_regress/t/t_mailbox_struct_param.v new file mode 100644 index 000000000..3b0d09cc0 --- /dev/null +++ b/test_regress/t/t_mailbox_struct_param.v @@ -0,0 +1,51 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Nikolai Kumar +// SPDX-License-Identifier: CC0-1.0 + +package pkg; + class C #(parameter P = 0); + typedef struct packed { + bit [7:0] x; + } my_t; + + mailbox #(my_t) mb = new(); + + task run(output bit [7:0] got); + my_t v; + mb.get(v); + got = v.x; + endtask + endclass +endpackage + +module top; + import pkg::*; + + initial begin + C #(0) c0; + C #(1) c1; + C#(0)::my_t s0; + C#(1)::my_t s1; + bit [7:0] got0; + bit [7:0] got1; + + c0 = new(); + c1 = new(); + + s0.x = 8'hA5; + s1.x = 8'h5A; + c0.mb.put(s0); + c1.mb.put(s1); + + c0.run(got0); + c1.run(got1); + + if(got0 !== 8'hA5) $stop; + if(got0 !== 8'hA5) $stop; + + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule From 76c1b26e3bb6eaac24b4e51610a3f731741f7fa9 Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Tue, 28 Apr 2026 21:31:08 +0200 Subject: [PATCH 013/350] Support `obj.randomize(null)` (#7487) (#7509) --- include/verilated_random.cpp | 78 +++++- include/verilated_random.h | 7 + src/V3Randomize.cpp | 105 ++++++++ src/V3Width.cpp | 11 +- .../t/t_randomize_inline_var_ctl_unsup_1.out | 4 - test_regress/t/t_randomize_null.py | 21 ++ test_regress/t/t_randomize_null.v | 251 ++++++++++++++++++ test_regress/t/t_randomize_null_unsup.out | 26 ++ test_regress/t/t_randomize_null_unsup.py | 16 ++ test_regress/t/t_randomize_null_unsup.v | 52 ++++ 10 files changed, 547 insertions(+), 24 deletions(-) create mode 100755 test_regress/t/t_randomize_null.py create mode 100644 test_regress/t/t_randomize_null.v create mode 100644 test_regress/t/t_randomize_null_unsup.out create mode 100755 test_regress/t/t_randomize_null_unsup.py create mode 100644 test_regress/t/t_randomize_null_unsup.v diff --git a/include/verilated_random.cpp b/include/verilated_random.cpp index a9d519266..93aba10f9 100644 --- a/include/verilated_random.cpp +++ b/include/verilated_random.cpp @@ -23,6 +23,7 @@ #include "verilated_random.h" +#include #include #include #include @@ -330,6 +331,30 @@ void VlRandomVar::emitExtract(std::ostream& s, int i) const { s << " ((_ extract " << i << ' ' << i << ") " << m_name << ')'; } void VlRandomVar::emitType(std::ostream& s) const { s << "(_ BitVec " << width() << ')'; } +// Serialize the current runtime value as an SMT-LIB binary literal. Used by +// randomize(null) to pin a var via `(assert (= var #b...))`. Binary (#b) +// rather than hex (#x) sidesteps SMT-LIB's hex-width-multiple-of-4 rule. +void VlRandomVar::emitConcreteValue(std::ostream& s) const { + const int w = width(); + const void* const dp = datap(0); + s << "#b"; + for (int i = w - 1; i >= 0; --i) { + int bit = 0; + if (w <= VL_BYTESIZE) { + bit = (*static_cast(dp) >> i) & 1; + } else if (w <= VL_SHORTSIZE) { + bit = (*static_cast(dp) >> i) & 1; + } else if (w <= VL_IDATASIZE) { + bit = (*static_cast(dp) >> i) & 1; + } else if (w <= VL_QUADSIZE) { + bit = (*static_cast(dp) >> i) & 1; + } else { + const EData* const wp = static_cast(dp); + bit = (wp[VL_BITWORD_E(i)] >> VL_BITBIT_E(i)) & 1; + } + s << (bit ? '1' : '0'); + } +} int VlRandomVar::totalWidth() const { return m_width; } static bool parseSMTNum(int obits, WDataOutP owp, const std::string& val) { int i; @@ -441,8 +466,16 @@ void VlRandomizer::recordRandcValues() { } } +bool VlRandomizer::next_check_only(VlRNG& rngr) { + m_checkOnly = true; + const bool result = next(rngr); + m_checkOnly = false; + return result; +} + bool VlRandomizer::next(VlRNG& rngr) { - if (m_vars.empty() && m_unique_arrays.empty()) return true; + if (!m_checkOnly && m_vars.empty() && m_unique_arrays.empty()) return true; + if (m_checkOnly && m_vars.empty()) return true; // No rand members: trivially SAT for (const std::string& baseName : m_unique_arrays) { const auto it = m_vars.find(baseName); const uint32_t size = m_unique_array_sizes.at(baseName); @@ -470,8 +503,8 @@ bool VlRandomizer::next(VlRNG& rngr) { } } - // If solve-before constraints are present, use phased solving - if (!m_solveBefore.empty()) return nextPhased(rngr); + // Pinned vars make phase ordering moot; skip phased path in check-only. + if (!m_checkOnly && !m_solveBefore.empty()) return nextPhased(rngr); // Randc retry: if unsat due to randc exhaustion, clear history and retry once const bool hasRandc = !m_randcVarNames.empty(); @@ -494,14 +527,24 @@ bool VlRandomizer::next(VlRNG& rngr) { os << "(declare-fun " << var.first << " () "; var.second->emitType(os); os << ")\n"; + // Pin each var to its current value: SAT iff the current values + // satisfy the constraints. V3Randomize rejects non-scalar rand + // members upstream, hence the assert. + if (m_checkOnly) { + assert(var.second->dimension() == 0); + os << "(assert (= " << var.first << ' '; + var.second->emitConcreteValue(os); + os << "))\n"; + } } for (const std::string& constraint : m_constraints) { os << "(assert (= #b1 " << constraint << "))\n"; } - // Randc: exclude previously used values to enforce cyclic non-repetition - emitRandcExclusions(os); + // randc exclusions vs. a pinned current value would make every check + // trivially UNSAT after the first cycle. + if (!m_checkOnly) emitRandcExclusions(os); const size_t nSoft = m_softConstraints.size(); bool sat = false; @@ -544,6 +587,10 @@ bool VlRandomizer::next(VlRNG& rngr) { m_randcUsedValues.clear(); continue; // Retry without exclusions } + // Skip the unsat-core path in check-only: it re-declares vars + // without pinning, so parseSolution would clobber user state with + // the solver's free assignment. + if (m_checkOnly) return false; // Genuine unsat: report via unsat-core os << "(set-option :produce-unsat-cores true)\n"; os << "(set-logic QF_ABV)\n"; @@ -569,17 +616,20 @@ bool VlRandomizer::next(VlRNG& rngr) { return false; } - for (int i = 0; i < _VL_SOLVER_HASH_LEN_TOTAL && sat; ++i) { - os << "(assert "; - randomConstraint(os, rngr, _VL_SOLVER_HASH_LEN); - os << ")\n"; - os << "\n(check-sat)\n"; - sat = parseSolution(os, false); - (void)sat; + // Pinned vars: salting cannot diversify, only burn solver calls. + if (!m_checkOnly) { + for (int i = 0; i < _VL_SOLVER_HASH_LEN_TOTAL && sat; ++i) { + os << "(assert "; + randomConstraint(os, rngr, _VL_SOLVER_HASH_LEN); + os << ")\n"; + os << "\n(check-sat)\n"; + sat = parseSolution(os, false); + (void)sat; + } } - // Record solved randc values for future exclusion - recordRandcValues(); + // Check-only must not advance randc cycle state. + if (!m_checkOnly) recordRandcValues(); os << "(reset)\n"; return true; diff --git a/include/verilated_random.h b/include/verilated_random.h index 0d82b5d78..6ab19a850 100644 --- a/include/verilated_random.h +++ b/include/verilated_random.h @@ -82,6 +82,9 @@ public: virtual void emitGetValue(std::ostream& s) const; virtual void emitExtract(std::ostream& s, int i) const; virtual void emitType(std::ostream& s) const; + // Emit the current runtime value as an SMT bit-vector literal (#b...). + // Used by randomize(null) to pin a var to its existing value. + virtual void emitConcreteValue(std::ostream& s) const; virtual int totalWidth() const; mutable std::shared_ptr m_arrVarsRefp; void setArrayInfo(const std::shared_ptr& arrVarsRefp) const { @@ -223,6 +226,7 @@ class VlRandomizer VL_NOT_FINAL { size_t m_randcConstraintHash = 0; // Hash of constraints when history was valid std::vector> m_solveBefore; // Solve-before ordering pairs (beforeVar, afterVar) + bool m_checkOnly = false; // Set for randomize(null) // PRIVATE METHODS void randomConstraint(std::ostream& os, VlRNG& rngr, int bits); @@ -241,6 +245,9 @@ public: // METHODS // Finds the next solution satisfying the constraints bool next(VlRNG& rngr); + // Validate the constraints against the current runtime values of every + // registered rand variable without picking new ones. + bool next_check_only(VlRNG& rngr); // --- Process the key for associative array --- diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index 67da65c05..e02b9e9fb 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -607,6 +607,28 @@ class RandomizeMarkVisitor final : public VNVisitor { return; } for (AstArg* argp = nodep->argsp(); argp; argp = VN_AS(argp->nextp(), Arg)) { + // randomize(null): handle before IS_RANDOMIZED_INLINE so the + // check-only path does not allocate unused __Vrandmode slots. + if (const AstConst* const constp = VN_CAST(argp->exprp(), Const)) { + UASSERT_OBJ(constp->num().isNull(), constp, + "Non-null AstConst arg to randomize() should have been " + "rejected by V3Width"); + // SMT pin only handles scalars; nested-class constraints don't cascade. + const bool hasUnsupportedMember + = classp->existsMember([](const AstClass*, const AstVar* memberVarp) { + if (!memberVarp->rand().isRandomizable()) return false; + const AstNodeDType* const dtp = memberVarp->dtypep()->skipRefp(); + return VN_IS(dtp, UnpackArrayDType) || VN_IS(dtp, DynArrayDType) + || VN_IS(dtp, QueueDType) || VN_IS(dtp, AssocArrayDType) + || VN_IS(dtp, WildcardArrayDType) || VN_IS(dtp, ClassRefDType); + }); + if (hasUnsupportedMember) { + nodep->v3warn(E_UNSUPPORTED, + "Unsupported: 'randomize(null)' on class with rand " + "container or class member"); + } + continue; + } classp->user1(IS_RANDOMIZED_INLINE); AstVar* fromVarp = nullptr; // If nodep is a method call, this is its receiver if (AstMethodCall* methodCallp = VN_CAST(nodep, MethodCall)) { @@ -4027,6 +4049,19 @@ class RandomizeVisitor final : public VNVisitor { // Handle inline random variable control. After this, the randomize() call has no args void handleRandomizeArgs(AstNodeFTaskRef* const nodep) { if (!nodep->argsp()) return; + // Strip the null literal arg. V3Width already rejected mixed/non-null + // AstConst args. + bool hasNullArg = false; + for (AstArg *argp = nodep->argsp(), *nextp = nullptr; argp; argp = nextp) { + nextp = VN_AS(argp->nextp(), Arg); + if (const AstConst* const constp = VN_CAST(argp->exprp(), Const)) { + UASSERT_OBJ(constp->num().isNull(), constp, + "Non-null AstConst arg to randomize() should have been " + "rejected by V3Width"); + hasNullArg = true; + VL_DO_DANGLING(argp->unlinkFrBack()->deleteTree(), argp); + } + } // This assumes arguments to always be a member sel from nodep->fromp(), if applicable // e.g. LinkDot transformed a.randomize(b, a.c) -> a.randomize(a.b, a.c) // Merge pins with common prefixes so that setting their rand mode doesn't interfere @@ -4089,6 +4124,24 @@ class RandomizeVisitor final : public VNVisitor { } argp->unlinkFrBack()->deleteTree(); } + if (hasNullArg) { // Re-point to the per-class __Vrandomize_null wrapper + AstClass* targetClassp = nullptr; + AstMethodCall* const methodCallp = VN_CAST(nodep, MethodCall); + if (methodCallp) { + const AstNodeDType* const fromDTypep = methodCallp->fromp()->dtypep(); + const AstClassRefDType* const crdtp + = VN_CAST(fromDTypep->skipRefp(), ClassRefDType); + UASSERT_OBJ(crdtp, nodep, "randomize(null) receiver is not a class type"); + targetClassp = crdtp->classp(); + } else { + targetClassp = VN_CAST(m_modp, Class); + } + UASSERT_OBJ(targetClassp, nodep, "randomize(null) target class unresolved"); + AstFunc* const checkOnlyFuncp = getCreateRandomizeNullFunc(targetClassp); + nodep->name(checkOnlyFuncp->name()); + nodep->taskp(checkOnlyFuncp); + nodep->dtypeFrom(checkOnlyFuncp->dtypep()); + } if (tmpVarps) { UASSERT_OBJ(storeStmtsp && setStmtsp && restoreStmtsp, nodep, "Should have stmts"); VNRelinker relinker; @@ -4102,6 +4155,58 @@ class RandomizeVisitor final : public VNVisitor { } } + // Create a class method `__Vrandomize_null` that implements the IEEE + // 1800-2023 18.11 semantic: validate all declared constraints against the + // current runtime values without assigning new ones. Body is: + // 1. pre_randomize() -- always (IEEE 1800-2023 18.6.2; 18.11 has no + // carve-out for the null case). + // 2. Compute result: + // - no constraints: `fvar = 1` (trivially satisfied; IEEE 18.11.1). + // - has constraints: clear solver constraints, re-run + // `__Vsetup_constraints`, then `fvar = gen.next_check_only(rng)`. + // 3. if (fvar) post_randomize() -- IEEE 1800-2023 18.6.3 says + // post_randomize is not called when randomize() fails. + AstFunc* getCreateRandomizeNullFunc(AstClass* const classp) { + static const char* const name = "__Vrandomize_null"; + if (AstFunc* const existingp = VN_AS(m_memberMap.findMember(classp, name), Func)) { + return existingp; + } + FileLine* const fl = classp->fileline(); + AstFunc* const funcp = V3Randomize::newRandomizeFunc(m_memberMap, classp, name); + AstVar* const fvarp = VN_AS(funcp->fvarp(), Var); + + // 1. pre_randomize -- always + addPrePostCall(classp, funcp, "pre_randomize"); + + // 2. Compute result + AstVar* const classGenp = getRandomGenerator(classp); + if (!classGenp) { + funcp->addStmtsp(new AstAssign{fl, new AstVarRef{fl, fvarp, VAccess::WRITE}, + new AstConst{fl, AstConst::WidthedValue{}, 32, 1}}); + } else { + AstNodeModule* const genModp = VN_AS(classGenp->user2p(), NodeModule); + funcp->addStmtsp(implementConstraintsClear(fl, classGenp)); + AstTask* const setupAllTaskp = getCreateConstraintSetupFunc(classp); + funcp->addStmtsp((new AstTaskRef{fl, setupAllTaskp})->makeStmt()); + AstCExpr* const solverCallp = new AstCExpr{fl}; + solverCallp->dtypeSetBit(); + solverCallp->add(new AstVarRef{fl, genModp, classGenp, VAccess::READWRITE}); + solverCallp->add(".next_check_only(__Vm_rng)"); + funcp->addStmtsp( + new AstAssign{fl, new AstVarRef{fl, fvarp, VAccess::WRITE}, solverCallp}); + classp->needRNG(true); + } + + // 3. post_randomize -- only if result is non-zero + if (AstTask* const userPostp = findPrePostTask(classp, "post_randomize")) { + AstTaskRef* const callp = new AstTaskRef{userPostp->fileline(), userPostp}; + funcp->addStmtsp( + new AstIf{fl, new AstVarRef{fl, fvarp, VAccess::READ}, callp->makeStmt()}); + } + + return funcp; + } + // Rewrite a LogIf-of-Dist chain into nested AstConstraintIf. The outermost // AstLogIf shell is left for the caller's AstConstraintExpr to free; inner // shells are deleted here once their children are transplanted. diff --git a/src/V3Width.cpp b/src/V3Width.cpp index c0ce22de3..713388321 100644 --- a/src/V3Width.cpp +++ b/src/V3Width.cpp @@ -4740,13 +4740,12 @@ class WidthVisitor final : public VNVisitor { VL_DO_DANGLING(argp->unlinkFrBack()->deleteTree(), argp); } } - if (nullp) { - if (hasNonNullArgs) { - nullp->v3error("Cannot pass more arguments to 'randomize(null)'"); - } else { - nullp->v3warn(E_UNSUPPORTED, "Unsupported: 'randomize(null)'"); - } + if (nullp && hasNonNullArgs) { + nullp->v3error("Cannot pass more arguments to 'randomize(null)'"); } + // A solo 'null' arg is left in place; V3Randomize lowers it into a + // check-only solve so constraints are validated against the current + // values of every rand member (IEEE 1800-2023 18.11). } void methodCallClass(AstMethodCall* nodep, AstClassRefDType* adtypep) { // No need to width-resolve the class, as it was done when we did the child diff --git a/test_regress/t/t_randomize_inline_var_ctl_unsup_1.out b/test_regress/t/t_randomize_inline_var_ctl_unsup_1.out index 1da1b0d5a..f2c0a7926 100644 --- a/test_regress/t/t_randomize_inline_var_ctl_unsup_1.out +++ b/test_regress/t/t_randomize_inline_var_ctl_unsup_1.out @@ -7,8 +7,4 @@ : ... note: In instance 't' 21 | void'(foo.randomize(foos[0].x)); | ^ -%Error-UNSUPPORTED: t/t_randomize_inline_var_ctl_unsup_1.v:22:25: Unsupported: 'randomize(null)' - : ... note: In instance 't' - 22 | void'(foo.randomize(null)); - | ^~~~ %Error: Exiting due to diff --git a/test_regress/t/t_randomize_null.py b/test_regress/t/t_randomize_null.py new file mode 100755 index 000000000..db1adb3f9 --- /dev/null +++ b/test_regress/t/t_randomize_null.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +if not test.have_solver: + test.skip("No constraint solver installed") + +test.compile() + +test.execute() + +test.passes() diff --git a/test_regress/t/t_randomize_null.v b/test_regress/t/t_randomize_null.v new file mode 100644 index 000000000..4c4955b1d --- /dev/null +++ b/test_regress/t/t_randomize_null.v @@ -0,0 +1,251 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +`define checkh(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +// verilog_format: on + +class A; + rand int x; + int v; + constraint c_lt {x < v;} +endclass + +class Multi; + rand int x; + rand int y; + int lo; + int hi; + constraint c_x {x >= lo; x <= hi;} + constraint c_y {y > x;} + + function int self_check; + return this.randomize(null); + endfunction +endclass + +class Trivial; + rand int p; + rand bit [3:0] q; +endclass + +class Base; + rand int x; + int v; + constraint c_base {x <= v;} +endclass + +class Derived extends Base; + rand int y; + constraint c_derived {y > x;} +endclass + +class Wide; + rand bit [64:0] w65; + rand bit [95:0] w96; + bit [64:0] lo65; + bit [95:0] lo96; + constraint c_wide {w65 >= lo65; w96 >= lo96;} +endclass + +// Cover the 16-bit (SData) and 64-bit (QData) tiers of +// VlRandomVar::emitConcreteValue's bit-extraction ladder, exercised when +// pinned current values are serialized to the SMT solver. +class Widths; + rand shortint s16; + rand longint l64; + shortint s_lo; + longint l_lo; + constraint c_widths {s16 >= s_lo; l64 >= l_lo;} +endclass + +class Cyc; + randc bit [1:0] c; + bit [1:0] lo; + constraint c_range {c >= lo;} +endclass + +// IEEE 1800-2023 18.6.2 / 18.6.3: pre_randomize is always called; post_randomize +// is called iff randomize() returned 1. +class Cb; + rand int x; + int v; + int pre_count; + int post_count; + constraint c_lt {x < v;} + function void pre_randomize; pre_count = pre_count + 1; endfunction + function void post_randomize; post_count = post_count + 1; endfunction +endclass + +module t; + A a; + Multi m; + Trivial triv; + Derived d; + Wide w; + Widths wd; + Cyc cyc; + Cb cb; + int i; + int ok0; + int ok1; + + initial begin + // 1. Original issue reproducer: unsat keeps values, sat preserves them. + a = new; + a.x = 2; a.v = 1; + i = a.randomize(null); + `checkd(i, 0); + `checkd(a.x, 2); + `checkd(a.v, 1); + + a.x = 1; a.v = 2; + i = a.randomize(null); + `checkd(i, 1); + `checkd(a.x, 1); + `checkd(a.v, 2); + + // 2. Multiple rand members, multiple constraints, plus implicit-this path. + m = new; + m.x = 5; m.y = 7; m.lo = 0; m.hi = 10; + i = m.randomize(null); + `checkd(i, 1); + `checkd(m.x, 5); + `checkd(m.y, 7); + + m.x = -1; m.y = 7; m.lo = 0; m.hi = 10; + i = m.randomize(null); + `checkd(i, 0); + `checkd(m.x, -1); + + m.x = 5; m.y = 5; m.lo = 0; m.hi = 10; + i = m.randomize(null); + `checkd(i, 0); + `checkd(m.y, 5); + + m.x = 3; m.y = 9; m.lo = 0; m.hi = 10; + i = m.self_check(); + `checkd(i, 1); + + // 3. Class with rand vars and no constraints: always sat, values untouched. + triv = new; + triv.p = 42; + triv.q = 4'h5; + i = triv.randomize(null); + `checkd(i, 1); + `checkd(triv.p, 42); + `checkh(triv.q, 4'h5); + + // 4. Inheritance: base and derived constraints validated together. + d = new; + d.x = 2; d.y = 5; d.v = 10; + i = d.randomize(null); + `checkd(i, 1); + `checkd(d.x, 2); + `checkd(d.y, 5); + + d.x = 11; d.y = 20; d.v = 10; + i = d.randomize(null); + `checkd(i, 0); + `checkd(d.x, 11); + + d.x = 3; d.y = 1; d.v = 10; + i = d.randomize(null); + `checkd(i, 0); + `checkd(d.y, 1); + + // 5. Wide (65-bit, 96-bit) rand vars: current-value pin must be bit-exact. + w = new; + w.w65 = 65'h1_0000_0000_0000_0000; + w.w96 = 96'hDEAD_BEEF_CAFE_0000_0000_0001; + w.lo65 = 65'h0_FFFF_FFFF_FFFF_FFFF; + w.lo96 = 96'h0; + i = w.randomize(null); + `checkd(i, 1); + `checkh(w.w65, 65'h1_0000_0000_0000_0000); + `checkh(w.w96, 96'hDEAD_BEEF_CAFE_0000_0000_0001); + + w.w65 = 65'h0_1234_5678_9ABC_DEF0; + w.lo65 = 65'h1_FFFF_FFFF_FFFF_FFFF; + i = w.randomize(null); + `checkd(i, 0); + `checkh(w.w65, 65'h0_1234_5678_9ABC_DEF0); + + // 5b. shortint (16-bit, SData) and longint (64-bit, QData) widths -- + // covers the middle tiers of emitConcreteValue's bit-extraction ladder. + wd = new; + wd.s16 = 16'sh1234; + wd.l64 = 64'sh0123_4567_89AB_CDEF; + wd.s_lo = 16'sh0; + wd.l_lo = 64'sh0; + i = wd.randomize(null); + `checkd(i, 1); + `checkh(wd.s16, 16'sh1234); + `checkh(wd.l64, 64'sh0123_4567_89AB_CDEF); + + wd.s16 = 16'sh0001; + wd.s_lo = 16'sh7FFF; + i = wd.randomize(null); + `checkd(i, 0); + `checkh(wd.s16, 16'sh0001); + + // 6. randc: null-call must NOT be poisoned by the exclusion history nor + // record values itself; a subsequent real randomize() must still cycle. + cyc = new; + cyc.lo = 2'd0; + repeat (4) begin + i = cyc.randomize(); + `checkd(i, 1); + end + + cyc.c = 2'd0; cyc.lo = 2'd0; + i = cyc.randomize(null); + `checkd(i, 1); + `checkd(cyc.c, 2'd0); + + cyc.c = 2'd3; cyc.lo = 2'd0; + i = cyc.randomize(null); + `checkd(i, 1); + `checkd(cyc.c, 2'd3); + + cyc.c = 2'd0; cyc.lo = 2'd1; + i = cyc.randomize(null); + `checkd(i, 0); + `checkd(cyc.c, 2'd0); + + cyc.lo = 2'd0; + ok0 = 0; ok1 = 0; + repeat (20) begin + i = cyc.randomize(); + `checkd(i, 1); + if (cyc.c == 2'd0) ok0 = 1; + if (cyc.c == 2'd1) ok1 = 1; + end + `checkd(ok0, 1); + `checkd(ok1, 1); + + // 7. pre_randomize / post_randomize observable behavior. + // IEEE 1800-2023 18.6.2: pre is always called. + // IEEE 1800-2023 18.6.3: post is called iff randomize() returned 1. + cb = new; + cb.x = 1; cb.v = 2; cb.pre_count = 0; cb.post_count = 0; + i = cb.randomize(null); // sat: pre + post + `checkd(i, 1); + `checkd(cb.pre_count, 1); + `checkd(cb.post_count, 1); + + cb.x = 5; cb.v = 1; + i = cb.randomize(null); // unsat: pre only, no post + `checkd(i, 0); + `checkd(cb.pre_count, 2); + `checkd(cb.post_count, 1); + + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_randomize_null_unsup.out b/test_regress/t/t_randomize_null_unsup.out new file mode 100644 index 000000000..7ffd08f0d --- /dev/null +++ b/test_regress/t/t_randomize_null_unsup.out @@ -0,0 +1,26 @@ +%Error-UNSUPPORTED: t/t_randomize_null_unsup.v:45:14: Unsupported: 'randomize(null)' on class with rand container or class member + : ... note: In instance 't' + 45 | void'(fq.randomize(null)); + | ^~~~~~~~~ + ... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest +%Error-UNSUPPORTED: t/t_randomize_null_unsup.v:46:14: Unsupported: 'randomize(null)' on class with rand container or class member + : ... note: In instance 't' + 46 | void'(fu.randomize(null)); + | ^~~~~~~~~ +%Error-UNSUPPORTED: t/t_randomize_null_unsup.v:47:14: Unsupported: 'randomize(null)' on class with rand container or class member + : ... note: In instance 't' + 47 | void'(fd.randomize(null)); + | ^~~~~~~~~ +%Error-UNSUPPORTED: t/t_randomize_null_unsup.v:48:14: Unsupported: 'randomize(null)' on class with rand container or class member + : ... note: In instance 't' + 48 | void'(fa.randomize(null)); + | ^~~~~~~~~ +%Error-UNSUPPORTED: t/t_randomize_null_unsup.v:49:14: Unsupported: 'randomize(null)' on class with rand container or class member + : ... note: In instance 't' + 49 | void'(fw.randomize(null)); + | ^~~~~~~~~ +%Error-UNSUPPORTED: t/t_randomize_null_unsup.v:50:13: Unsupported: 'randomize(null)' on class with rand container or class member + : ... note: In instance 't' + 50 | void'(o.randomize(null)); + | ^~~~~~~~~ +%Error: Exiting due to diff --git a/test_regress/t/t_randomize_null_unsup.py b/test_regress/t/t_randomize_null_unsup.py new file mode 100755 index 000000000..02d2f5abe --- /dev/null +++ b/test_regress/t/t_randomize_null_unsup.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.lint(fails=test.vlt_all, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_randomize_null_unsup.v b/test_regress/t/t_randomize_null_unsup.v new file mode 100644 index 000000000..ad9e710a1 --- /dev/null +++ b/test_regress/t/t_randomize_null_unsup.v @@ -0,0 +1,52 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +class FooQ; + rand int q[$]; +endclass + +class FooU; + rand int u[3]; +endclass + +class FooD; + rand int d[]; +endclass + +class FooA; + rand int a[string]; +endclass + +class FooW; + rand int w[*]; +endclass + +class Inner; + rand int x; + constraint c_in {x > 0;} +endclass + +class Outer; + rand Inner inner; +endclass + +module t; + initial begin + automatic FooQ fq = new; + automatic FooU fu = new; + automatic FooD fd = new; + automatic FooA fa = new; + automatic FooW fw = new; + automatic Outer o = new; + o.inner = new; + void'(fq.randomize(null)); + void'(fu.randomize(null)); + void'(fd.randomize(null)); + void'(fa.randomize(null)); + void'(fw.randomize(null)); + void'(o.randomize(null)); + end +endmodule From 01886796315172e833fa136c8804cd6d39d2933b Mon Sep 17 00:00:00 2001 From: Todd Strader Date: Tue, 28 Apr 2026 15:33:41 -0400 Subject: [PATCH 014/350] Fix expression coverage in loops (#7511) --- src/V3Coverage.cpp | 2 ++ test_regress/t/t_cover_expr.out | 10 ++++++++-- test_regress/t/t_cover_expr.v | 3 +++ test_regress/t/t_cover_expr_max.out | 10 ++++++++-- test_regress/t/t_cover_expr_trace.out | 10 ++++++++-- test_regress/t/t_cover_line_expr.out | 14 ++++++++++++-- 6 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/V3Coverage.cpp b/src/V3Coverage.cpp index 7f25d6b5b..c39e613f9 100644 --- a/src/V3Coverage.cpp +++ b/src/V3Coverage.cpp @@ -300,7 +300,9 @@ class CoverageVisitor final : public VNVisitor { void visit(AstLoop* nodep) override { UASSERT_OBJ(!nodep->contsp(), nodep, "'contsp' only used before LinkJump"); VL_RESTORER(m_state); + VL_RESTORER(m_exprStmtsp); VL_RESTORER(m_inToggleOff); + m_exprStmtsp = nodep; m_inToggleOff = true; createHandle(nodep); iterateAndNextNull(nodep->stmtsp()); diff --git a/test_regress/t/t_cover_expr.out b/test_regress/t/t_cover_expr.out index 9acf096c7..53fc1ef61 100644 --- a/test_regress/t/t_cover_expr.out +++ b/test_regress/t/t_cover_expr.out @@ -209,6 +209,12 @@ -000000 point: type=expr comment=(t2==0) => 0 hier=top.t end end + for (int loop_var_2 = 0; loop_var_2 < 32; loop_var_2 += 2) begin +~000131 if (cyc[loop_var_2] | cyc[loop_var_2 + 1]) $write(""); +-000006 point: type=expr comment=(cyc[(loop_var_2 + 32'sh1)[4:0]+:1]==1) => 1 hier=top.t ++000131 point: type=expr comment=(cyc[loop_var_2[4:0]+:1]==0 && cyc[(loop_var_2 + 32'sh1)[4:0]+:1]==0) => 0 hier=top.t +-000009 point: type=expr comment=(cyc[loop_var_2[4:0]+:1]==1) => 1 hier=top.t + end // stop at the first layer even if there's more to find %000007 if ((cyc[3+32'(t1&&t2)+:2] == cyc[5+32'(t3||t4)+:2]) || cyc[31]) $write(""); -000002 point: type=expr comment=((cyc[(32'sh3 + (t1 && t2))[4:0]+:2] == cyc[(32'sh5 + (t3 || t4))[4:0]+:2])==0 && cyc[31]==0) => 0 hier=top.t @@ -295,8 +301,8 @@ %000001 while (ta || tb || tc) begin -000001 point: type=expr comment=(ta==0 && tb==0 && tc==0) => 0 hier=top.t -000000 point: type=expr comment=(ta==1) => 1 hier=top.t --000000 point: type=expr comment=(tb==1) => 1 hier=top.t --000000 point: type=expr comment=(tc==1) => 1 hier=top.t +-000001 point: type=expr comment=(tb==1) => 1 hier=top.t +-000001 point: type=expr comment=(tc==1) => 1 hier=top.t tc = tb; tb = ta; ta = '0; diff --git a/test_regress/t/t_cover_expr.v b/test_regress/t/t_cover_expr.v index ff5ecc410..d51e9ccee 100644 --- a/test_regress/t/t_cover_expr.v +++ b/test_regress/t/t_cover_expr.v @@ -86,6 +86,9 @@ module t ( if (cyc[loop_var] && t2) $write(""); end end + for (int loop_var_2 = 0; loop_var_2 < 32; loop_var_2 += 2) begin + if (cyc[loop_var_2] | cyc[loop_var_2 + 1]) $write(""); + end // stop at the first layer even if there's more to find if ((cyc[3+32'(t1&&t2)+:2] == cyc[5+32'(t3||t4)+:2]) || cyc[31]) $write(""); // impossible branches and redundant terms diff --git a/test_regress/t/t_cover_expr_max.out b/test_regress/t/t_cover_expr_max.out index c1faed6a8..974946ae5 100644 --- a/test_regress/t/t_cover_expr_max.out +++ b/test_regress/t/t_cover_expr_max.out @@ -209,6 +209,12 @@ -000000 point: type=expr comment=(t2==0) => 0 hier=top.t end end + for (int loop_var_2 = 0; loop_var_2 < 32; loop_var_2 += 2) begin +~000131 if (cyc[loop_var_2] | cyc[loop_var_2 + 1]) $write(""); +-000006 point: type=expr comment=(cyc[(loop_var_2 + 32'sh1)[4:0]+:1]==1) => 1 hier=top.t ++000131 point: type=expr comment=(cyc[loop_var_2[4:0]+:1]==0 && cyc[(loop_var_2 + 32'sh1)[4:0]+:1]==0) => 0 hier=top.t +-000009 point: type=expr comment=(cyc[loop_var_2[4:0]+:1]==1) => 1 hier=top.t + end // stop at the first layer even if there's more to find %000007 if ((cyc[3+32'(t1&&t2)+:2] == cyc[5+32'(t3||t4)+:2]) || cyc[31]) $write(""); -000002 point: type=expr comment=((cyc[(32'sh3 + (t1 && t2))[4:0]+:2] == cyc[(32'sh5 + (t3 || t4))[4:0]+:2])==0 && cyc[31]==0) => 0 hier=top.t @@ -423,8 +429,8 @@ %000001 while (ta || tb || tc) begin -000001 point: type=expr comment=(ta==0 && tb==0 && tc==0) => 0 hier=top.t -000000 point: type=expr comment=(ta==1) => 1 hier=top.t --000000 point: type=expr comment=(tb==1) => 1 hier=top.t --000000 point: type=expr comment=(tc==1) => 1 hier=top.t +-000001 point: type=expr comment=(tb==1) => 1 hier=top.t +-000001 point: type=expr comment=(tc==1) => 1 hier=top.t tc = tb; tb = ta; ta = '0; diff --git a/test_regress/t/t_cover_expr_trace.out b/test_regress/t/t_cover_expr_trace.out index 9acf096c7..53fc1ef61 100644 --- a/test_regress/t/t_cover_expr_trace.out +++ b/test_regress/t/t_cover_expr_trace.out @@ -209,6 +209,12 @@ -000000 point: type=expr comment=(t2==0) => 0 hier=top.t end end + for (int loop_var_2 = 0; loop_var_2 < 32; loop_var_2 += 2) begin +~000131 if (cyc[loop_var_2] | cyc[loop_var_2 + 1]) $write(""); +-000006 point: type=expr comment=(cyc[(loop_var_2 + 32'sh1)[4:0]+:1]==1) => 1 hier=top.t ++000131 point: type=expr comment=(cyc[loop_var_2[4:0]+:1]==0 && cyc[(loop_var_2 + 32'sh1)[4:0]+:1]==0) => 0 hier=top.t +-000009 point: type=expr comment=(cyc[loop_var_2[4:0]+:1]==1) => 1 hier=top.t + end // stop at the first layer even if there's more to find %000007 if ((cyc[3+32'(t1&&t2)+:2] == cyc[5+32'(t3||t4)+:2]) || cyc[31]) $write(""); -000002 point: type=expr comment=((cyc[(32'sh3 + (t1 && t2))[4:0]+:2] == cyc[(32'sh5 + (t3 || t4))[4:0]+:2])==0 && cyc[31]==0) => 0 hier=top.t @@ -295,8 +301,8 @@ %000001 while (ta || tb || tc) begin -000001 point: type=expr comment=(ta==0 && tb==0 && tc==0) => 0 hier=top.t -000000 point: type=expr comment=(ta==1) => 1 hier=top.t --000000 point: type=expr comment=(tb==1) => 1 hier=top.t --000000 point: type=expr comment=(tc==1) => 1 hier=top.t +-000001 point: type=expr comment=(tb==1) => 1 hier=top.t +-000001 point: type=expr comment=(tc==1) => 1 hier=top.t tc = tb; tb = ta; ta = '0; diff --git a/test_regress/t/t_cover_line_expr.out b/test_regress/t/t_cover_line_expr.out index b74986880..59f60ceb8 100644 --- a/test_regress/t/t_cover_line_expr.out +++ b/test_regress/t/t_cover_line_expr.out @@ -285,6 +285,16 @@ -000000 point: type=branch comment=if hier=top.t end end +~000144 for (int loop_var_2 = 0; loop_var_2 < 32; loop_var_2 += 2) begin +-000009 point: type=line comment=block hier=top.t ++000144 point: type=line comment=block hier=top.t +~000131 if (cyc[loop_var_2] | cyc[loop_var_2 + 1]) $write(""); +-000006 point: type=expr comment=(cyc[(loop_var_2 + 32'sh1)[4:0]+:1]==1) => 1 hier=top.t ++000131 point: type=expr comment=(cyc[loop_var_2[4:0]+:1]==0 && cyc[(loop_var_2 + 32'sh1)[4:0]+:1]==0) => 0 hier=top.t +-000009 point: type=expr comment=(cyc[loop_var_2[4:0]+:1]==1) => 1 hier=top.t ++000013 point: type=branch comment=if hier=top.t ++000131 point: type=branch comment=else hier=top.t + end // stop at the first layer even if there's more to find %000007 if ((cyc[3+32'(t1&&t2)+:2] == cyc[5+32'(t3||t4)+:2]) || cyc[31]) $write(""); -000007 point: type=branch comment=if hier=top.t @@ -411,8 +421,8 @@ %000003 while (ta || tb || tc) begin -000001 point: type=expr comment=(ta==0 && tb==0 && tc==0) => 0 hier=top.t -000000 point: type=expr comment=(ta==1) => 1 hier=top.t --000000 point: type=expr comment=(tb==1) => 1 hier=top.t --000000 point: type=expr comment=(tc==1) => 1 hier=top.t +-000001 point: type=expr comment=(tb==1) => 1 hier=top.t +-000001 point: type=expr comment=(tc==1) => 1 hier=top.t -000003 point: type=line comment=block hier=top.t %000003 tc = tb; -000003 point: type=line comment=block hier=top.t From 6aebcd2b1c74dac30551692c2081a174d0f1d496 Mon Sep 17 00:00:00 2001 From: Wilson Snyder Date: Tue, 28 Apr 2026 17:56:24 -0400 Subject: [PATCH 015/350] Commentary: Changes update --- Changes | 11 ++- test_regress/t/t_interface_func_impure_bad.v | 2 +- ..._interface_virtual_func_runtime_instance.v | 4 +- .../t_interface_virtual_func_static_direct.v | 2 +- test_regress/t/t_mailbox_struct_param.v | 16 ++-- test_regress/t/t_randomize_null.v | 85 ++++++++++++++----- test_regress/t/t_randomize_null_unsup.v | 12 +-- .../t/t_std_randomize_static_member.v | 50 +++++------ 8 files changed, 117 insertions(+), 65 deletions(-) diff --git a/Changes b/Changes index c24ba6440..6ad820bff 100644 --- a/Changes +++ b/Changes @@ -11,7 +11,16 @@ contributors that suggested or implemented a given issue are shown in []. Thanks Verilator 5.049 devel ========================== -* TBD +* Support `always` / `always[m:n]` / `s_always[m:n]` property operators (#7482). [Yilou Wang] +* Support `randomize() with (identifier_list) {constraint_block}` (#7486) (#7507). [Yilou Wang] +* Support `obj.randomize(null)` (#7487) (#7509). [Yilou Wang] +* Fix inlining static initializer in V3Gate (#5381) (#7503). [Andrew Nolte] [Geza Lore, Testorrent USA, Inc.] +* Fix generic interface port forwarded to a nested instance (#7454) (#7457). [Yilou Wang] +* Fix internal error on multi-cycle SVA under default clocking (#7472) (#7506). [Yilou Wang] +* Fix mailbox#(packed_struct) type mismatch with parameterized class (#7494) (#7495). [Nikolai Kumar] +* Fix std::randomize internal error on static member of different class (#7498) (#7499). [Alex Solomatnikov] +* Fix virtual interface method call inlining and IMPURE suppression (#7505). [Nikolay Puzanov] +* Fix expression coverage in loops (#7511). [Todd Strader] Verilator 5.048 2026-04-26 diff --git a/test_regress/t/t_interface_func_impure_bad.v b/test_regress/t/t_interface_func_impure_bad.v index 6ef962e15..07e741618 100644 --- a/test_regress/t/t_interface_func_impure_bad.v +++ b/test_regress/t/t_interface_func_impure_bad.v @@ -25,7 +25,7 @@ interface iface; endinterface module t; - iface i(); + iface i (); initial begin i.local_write(); diff --git a/test_regress/t/t_interface_virtual_func_runtime_instance.v b/test_regress/t/t_interface_virtual_func_runtime_instance.v index 1ad4e60e0..a65626ff9 100644 --- a/test_regress/t/t_interface_virtual_func_runtime_instance.v +++ b/test_regress/t/t_interface_virtual_func_runtime_instance.v @@ -21,8 +21,8 @@ class Driver; endclass module t; - iface a(); - iface b(); + iface a (); + iface b (); initial begin automatic Driver d = new; diff --git a/test_regress/t/t_interface_virtual_func_static_direct.v b/test_regress/t/t_interface_virtual_func_static_direct.v index 6d8d6cd12..6a08c220d 100644 --- a/test_regress/t/t_interface_virtual_func_static_direct.v +++ b/test_regress/t/t_interface_virtual_func_static_direct.v @@ -16,7 +16,7 @@ class Holder; endclass module t; - iface i(); + iface i (); initial begin automatic Holder h = new; diff --git a/test_regress/t/t_mailbox_struct_param.v b/test_regress/t/t_mailbox_struct_param.v index 3b0d09cc0..fbb304556 100644 --- a/test_regress/t/t_mailbox_struct_param.v +++ b/test_regress/t/t_mailbox_struct_param.v @@ -5,10 +5,10 @@ // SPDX-License-Identifier: CC0-1.0 package pkg; - class C #(parameter P = 0); - typedef struct packed { - bit [7:0] x; - } my_t; + class C #( + parameter P = 0 + ); + typedef struct packed {bit [7:0] x;} my_t; mailbox #(my_t) mb = new(); @@ -26,8 +26,8 @@ module top; initial begin C #(0) c0; C #(1) c1; - C#(0)::my_t s0; - C#(1)::my_t s1; + C #(0)::my_t s0; + C #(1)::my_t s1; bit [7:0] got0; bit [7:0] got1; @@ -42,8 +42,8 @@ module top; c0.run(got0); c1.run(got1); - if(got0 !== 8'hA5) $stop; - if(got0 !== 8'hA5) $stop; + if (got0 !== 8'hA5) $stop; + if (got0 !== 8'hA5) $stop; $write("*-* All Finished *-*\n"); $finish; diff --git a/test_regress/t/t_randomize_null.v b/test_regress/t/t_randomize_null.v index 4c4955b1d..e8df1b522 100644 --- a/test_regress/t/t_randomize_null.v +++ b/test_regress/t/t_randomize_null.v @@ -21,7 +21,10 @@ class Multi; rand int y; int lo; int hi; - constraint c_x {x >= lo; x <= hi;} + constraint c_x { + x >= lo; + x <= hi; + } constraint c_y {y > x;} function int self_check; @@ -50,7 +53,10 @@ class Wide; rand bit [95:0] w96; bit [64:0] lo65; bit [95:0] lo96; - constraint c_wide {w65 >= lo65; w96 >= lo96;} + constraint c_wide { + w65 >= lo65; + w96 >= lo96; + } endclass // Cover the 16-bit (SData) and 64-bit (QData) tiers of @@ -58,10 +64,13 @@ endclass // pinned current values are serialized to the SMT solver. class Widths; rand shortint s16; - rand longint l64; + rand longint l64; shortint s_lo; - longint l_lo; - constraint c_widths {s16 >= s_lo; l64 >= l_lo;} + longint l_lo; + constraint c_widths { + s16 >= s_lo; + l64 >= l_lo; + } endclass class Cyc; @@ -78,8 +87,12 @@ class Cb; int pre_count; int post_count; constraint c_lt {x < v;} - function void pre_randomize; pre_count = pre_count + 1; endfunction - function void post_randomize; post_count = post_count + 1; endfunction + function void pre_randomize; + pre_count = pre_count + 1; + endfunction + function void post_randomize; + post_count = post_count + 1; + endfunction endclass module t; @@ -98,13 +111,15 @@ module t; initial begin // 1. Original issue reproducer: unsat keeps values, sat preserves them. a = new; - a.x = 2; a.v = 1; + a.x = 2; + a.v = 1; i = a.randomize(null); `checkd(i, 0); `checkd(a.x, 2); `checkd(a.v, 1); - a.x = 1; a.v = 2; + a.x = 1; + a.v = 2; i = a.randomize(null); `checkd(i, 1); `checkd(a.x, 1); @@ -112,23 +127,35 @@ module t; // 2. Multiple rand members, multiple constraints, plus implicit-this path. m = new; - m.x = 5; m.y = 7; m.lo = 0; m.hi = 10; + m.x = 5; + m.y = 7; + m.lo = 0; + m.hi = 10; i = m.randomize(null); `checkd(i, 1); `checkd(m.x, 5); `checkd(m.y, 7); - m.x = -1; m.y = 7; m.lo = 0; m.hi = 10; + m.x = -1; + m.y = 7; + m.lo = 0; + m.hi = 10; i = m.randomize(null); `checkd(i, 0); `checkd(m.x, -1); - m.x = 5; m.y = 5; m.lo = 0; m.hi = 10; + m.x = 5; + m.y = 5; + m.lo = 0; + m.hi = 10; i = m.randomize(null); `checkd(i, 0); `checkd(m.y, 5); - m.x = 3; m.y = 9; m.lo = 0; m.hi = 10; + m.x = 3; + m.y = 9; + m.lo = 0; + m.hi = 10; i = m.self_check(); `checkd(i, 1); @@ -143,18 +170,24 @@ module t; // 4. Inheritance: base and derived constraints validated together. d = new; - d.x = 2; d.y = 5; d.v = 10; + d.x = 2; + d.y = 5; + d.v = 10; i = d.randomize(null); `checkd(i, 1); `checkd(d.x, 2); `checkd(d.y, 5); - d.x = 11; d.y = 20; d.v = 10; + d.x = 11; + d.y = 20; + d.v = 10; i = d.randomize(null); `checkd(i, 0); `checkd(d.x, 11); - d.x = 3; d.y = 1; d.v = 10; + d.x = 3; + d.y = 1; + d.v = 10; i = d.randomize(null); `checkd(i, 0); `checkd(d.y, 1); @@ -203,23 +236,27 @@ module t; `checkd(i, 1); end - cyc.c = 2'd0; cyc.lo = 2'd0; + cyc.c = 2'd0; + cyc.lo = 2'd0; i = cyc.randomize(null); `checkd(i, 1); `checkd(cyc.c, 2'd0); - cyc.c = 2'd3; cyc.lo = 2'd0; + cyc.c = 2'd3; + cyc.lo = 2'd0; i = cyc.randomize(null); `checkd(i, 1); `checkd(cyc.c, 2'd3); - cyc.c = 2'd0; cyc.lo = 2'd1; + cyc.c = 2'd0; + cyc.lo = 2'd1; i = cyc.randomize(null); `checkd(i, 0); `checkd(cyc.c, 2'd0); cyc.lo = 2'd0; - ok0 = 0; ok1 = 0; + ok0 = 0; + ok1 = 0; repeat (20) begin i = cyc.randomize(); `checkd(i, 1); @@ -233,13 +270,17 @@ module t; // IEEE 1800-2023 18.6.2: pre is always called. // IEEE 1800-2023 18.6.3: post is called iff randomize() returned 1. cb = new; - cb.x = 1; cb.v = 2; cb.pre_count = 0; cb.post_count = 0; + cb.x = 1; + cb.v = 2; + cb.pre_count = 0; + cb.post_count = 0; i = cb.randomize(null); // sat: pre + post `checkd(i, 1); `checkd(cb.pre_count, 1); `checkd(cb.post_count, 1); - cb.x = 5; cb.v = 1; + cb.x = 5; + cb.v = 1; i = cb.randomize(null); // unsat: pre only, no post `checkd(i, 0); `checkd(cb.pre_count, 2); diff --git a/test_regress/t/t_randomize_null_unsup.v b/test_regress/t/t_randomize_null_unsup.v index ad9e710a1..1d86f72dd 100644 --- a/test_regress/t/t_randomize_null_unsup.v +++ b/test_regress/t/t_randomize_null_unsup.v @@ -35,12 +35,12 @@ endclass module t; initial begin - automatic FooQ fq = new; - automatic FooU fu = new; - automatic FooD fd = new; - automatic FooA fa = new; - automatic FooW fw = new; - automatic Outer o = new; + automatic FooQ fq = new; + automatic FooU fu = new; + automatic FooD fd = new; + automatic FooA fa = new; + automatic FooW fw = new; + automatic Outer o = new; o.inner = new; void'(fq.randomize(null)); void'(fu.randomize(null)); diff --git a/test_regress/t/t_std_randomize_static_member.v b/test_regress/t/t_std_randomize_static_member.v index c80d3336a..af050c01b 100644 --- a/test_regress/t/t_std_randomize_static_member.v +++ b/test_regress/t/t_std_randomize_static_member.v @@ -10,36 +10,38 @@ // in V3Randomize ("Invalid reference?"). package tlogy_m_pkg; -class v_cfg; - static int num_of_ds = 0; -endclass + class v_cfg; + static int num_of_ds = 0; + endclass -class tlogy_m; - v_cfg v[$]; -endclass + class tlogy_m; + v_cfg v[$]; + endclass endpackage package s_pkg; -import tlogy_m_pkg::*; -class s_cfg; - tlogy_m t_m; - bit t_mode; - int a_d_idx; + import tlogy_m_pkg::*; + class s_cfg; + tlogy_m t_m; + bit t_mode; + int a_d_idx; - function void setup_h_iw_cfg(); - if (t_mode) begin - foreach (t_m.v[i]) begin - if (std::randomize(a_d_idx) with { - if (t_m.v[i].num_of_ds > 1) { - a_d_idx inside {[0:(t_m.v[i].num_of_ds - 1)]}; - a_d_idx != 0; - } - } == 0) - $stop; + function void setup_h_iw_cfg(); + if (t_mode) begin + foreach (t_m.v[i]) begin + if (std::randomize( + a_d_idx + ) with { + if (t_m.v[i].num_of_ds > 1) { + a_d_idx inside {[0 : (t_m.v[i].num_of_ds - 1)]}; + a_d_idx != 0; + } + } == 0) + $stop; + end end - end - endfunction -endclass + endfunction + endclass endpackage module t; From 8f18f0cf225245eb7fc380957e4f06d65e46e44c Mon Sep 17 00:00:00 2001 From: Wilson Snyder Date: Tue, 28 Apr 2026 17:59:18 -0400 Subject: [PATCH 016/350] Fix internal error instead of missing prototype error (#7485). [Alex Solomatnikov] Fixes #7485. --- Changes | 1 + src/V3LinkDot.cpp | 1 + src/V3SymTable.h | 11 ++++++++++- test_regress/t/t_class_extern_mis2_bad.out | 5 +++++ test_regress/t/t_class_extern_mis2_bad.py | 16 ++++++++++++++++ test_regress/t/t_class_extern_mis2_bad.v | 21 +++++++++++++++++++++ 6 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 test_regress/t/t_class_extern_mis2_bad.out create mode 100755 test_regress/t/t_class_extern_mis2_bad.py create mode 100644 test_regress/t/t_class_extern_mis2_bad.v diff --git a/Changes b/Changes index 6ad820bff..6f3f2c62c 100644 --- a/Changes +++ b/Changes @@ -17,6 +17,7 @@ Verilator 5.049 devel * Fix inlining static initializer in V3Gate (#5381) (#7503). [Andrew Nolte] [Geza Lore, Testorrent USA, Inc.] * Fix generic interface port forwarded to a nested instance (#7454) (#7457). [Yilou Wang] * Fix internal error on multi-cycle SVA under default clocking (#7472) (#7506). [Yilou Wang] +* Fix internal error instead of missing prototype error (#7485). [Alex Solomatnikov] * Fix mailbox#(packed_struct) type mismatch with parameterized class (#7494) (#7495). [Nikolai Kumar] * Fix std::randomize internal error on static member of different class (#7498) (#7499). [Alex Solomatnikov] * Fix virtual interface method call inlining and IMPURE suppression (#7505). [Nikolay Puzanov] diff --git a/src/V3LinkDot.cpp b/src/V3LinkDot.cpp index 758352a7f..66b9abcc6 100644 --- a/src/V3LinkDot.cpp +++ b/src/V3LinkDot.cpp @@ -193,6 +193,7 @@ public: void dumpSelf(const string& nameComment, bool force = false) { if (debug() >= 6 || dumpLevel() >= 6 || force) { const string filename = v3Global.debugFilename(nameComment) + ".txt"; + UINFO(4, "Dumping " << filename); const std::unique_ptr logp{V3File::new_ofstream(filename)}; if (logp->fail()) v3fatal("Can't write file: " << filename); std::ostream& os = *logp; diff --git a/src/V3SymTable.h b/src/V3SymTable.h index dbb5b383d..f3cb1bfb1 100644 --- a/src/V3SymTable.h +++ b/src/V3SymTable.h @@ -207,9 +207,18 @@ public: void importFromClass(VSymGraph* graphp, const VSymEnt* srcp) { // Import tokens from source symbol table into this symbol table // Used for classes in early parsing only to handle "extends" + + // If an "extern foo" exists, then we can't import "foo" from the base class. + // But ok for "extern foo" and "foo" to both come from base (so must check before insert) + std::unordered_set haveExterns; for (IdNameMap::const_iterator it = srcp->m_idNameMap.begin(); it != srcp->m_idNameMap.end(); ++it) { - importOneSymbol(graphp, it->first, it->second, false); + if (m_idNameMap.count("extern " + it->first)) haveExterns.emplace(it->first); + } + for (IdNameMap::const_iterator it = srcp->m_idNameMap.begin(); + it != srcp->m_idNameMap.end(); ++it) { + if (!haveExterns.count(it->first)) + importOneSymbol(graphp, it->first, it->second, false); } } void importFromPackage(VSymGraph* graphp, const VSymEnt* srcp, const string& id_or_star) { diff --git a/test_regress/t/t_class_extern_mis2_bad.out b/test_regress/t/t_class_extern_mis2_bad.out new file mode 100644 index 000000000..2382ded8b --- /dev/null +++ b/test_regress/t/t_class_extern_mis2_bad.out @@ -0,0 +1,5 @@ +%Error-PROTOTYPEMIS: t/t_class_extern_mis2_bad.v:15:21: Definition not found for extern prototype 'new' + 15 | extern function new(); + | ^~~ + ... For error description see https://verilator.org/warn/PROTOTYPEMIS?v=latest +%Error: Exiting due to diff --git a/test_regress/t/t_class_extern_mis2_bad.py b/test_regress/t/t_class_extern_mis2_bad.py new file mode 100755 index 000000000..38cf36b43 --- /dev/null +++ b/test_regress/t/t_class_extern_mis2_bad.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('linter') + +test.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_class_extern_mis2_bad.v b/test_regress/t/t_class_extern_mis2_bad.v new file mode 100644 index 000000000..a7985be14 --- /dev/null +++ b/test_regress/t/t_class_extern_mis2_bad.v @@ -0,0 +1,21 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +package s_core_env_pkg; + virtual class x_scoreboard; + extern function void has_ext_ok(); + endclass + function void x_scoreboard::has_ext_ok(); + endfunction + + class cls_misses_new_1 extends x_scoreboard; + extern function new(); // <--- BAD + endclass + +endpackage + +module t; +endmodule From 4ebc4f1eeeebc19fd056da2cad99116aaa159c1d Mon Sep 17 00:00:00 2001 From: Nikolai Kumar Date: Wed, 29 Apr 2026 04:18:12 -0500 Subject: [PATCH 017/350] Fix V3Assert stale failsp after recursive iteration (#7500) (#7513) Fixes #7500. --- src/V3Assert.cpp | 5 +++ test_regress/t/t_randomize_inside_cond.py | 21 +++++++++++++ test_regress/t/t_randomize_inside_cond.v | 38 +++++++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100755 test_regress/t/t_randomize_inside_cond.py create mode 100644 test_regress/t/t_randomize_inside_cond.v diff --git a/src/V3Assert.cpp b/src/V3Assert.cpp index 22eea94a4..278e2995f 100644 --- a/src/V3Assert.cpp +++ b/src/V3Assert.cpp @@ -429,6 +429,11 @@ class AssertVisitor final : public VNVisitor { const string& message = nodep->name(); AstNode* passsp = nodep->passsp(); if (passsp) passsp->unlinkFrBackWithNext(); + if (AstAssert* const assertp = VN_CAST(nodep, Assert)) { + failsp = assertp->failsp(); + } else if (AstAssertIntrinsic* const assertp = VN_CAST(nodep, AssertIntrinsic)) { + failsp = assertp->failsp(); + } if (failsp) failsp->unlinkFrBackWithNext(); bool selfDestruct = false; diff --git a/test_regress/t/t_randomize_inside_cond.py b/test_regress/t/t_randomize_inside_cond.py new file mode 100755 index 000000000..81df8362d --- /dev/null +++ b/test_regress/t/t_randomize_inside_cond.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +if not test.have_solver: + test.skip("No constraint solver installed") + +test.compile(verilator_flags2=["--binary"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_randomize_inside_cond.v b/test_regress/t/t_randomize_inside_cond.v new file mode 100644 index 000000000..694b161e9 --- /dev/null +++ b/test_regress/t/t_randomize_inside_cond.v @@ -0,0 +1,38 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Nikolai Kumar +// SPDX-License-Identifier: CC0-1.0 + +module t; + int x; + int pass_count, fail_count; + + initial begin + pass_count = 0; + fail_count = 0; + + assert((std::randomize(x) with {x inside {10, 20}; }) == 1) + pass_count++; + else + assert((std::randomize(x) with {x inside {1, 2}; }) == 1) + fail_count++; //Should not run + + assert((std::randomize(x) with {x > 100; x inside {1, 2 , 3}; }) == 1) + pass_count++; //Should not run + else + assert((std::randomize(x) with {x inside {40, 50}; }) == 1) + fail_count++; + + if(pass_count != 1) begin + $display("FAIL: pass_count=%0d expected 1", pass_count); + $stop; + end + if(fail_count != 1) begin + $display("FAIL: fail_count=%0d expected 1", fail_count); + $stop; + end + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule From 30edb987d2fc5e2ab63132ba752b859da975ef37 Mon Sep 17 00:00:00 2001 From: Wilson Snyder Date: Wed, 29 Apr 2026 07:52:43 -0400 Subject: [PATCH 018/350] CI: Add automatic code coverage references --- .github/workflows/coverage.yml | 2 ++ docs/internals.rst | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7c1bd1ec3..1d71ec481 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -163,6 +163,8 @@ jobs: grep -E "(lines|branches)\.*:" ${{ github.workspace }}/make-coverage-report.log | sed "s/\.*:/:/" >> notification/body.txt || true echo "" >> notification/body.txt echo "Report: [${{ github.run_id }}](https://${{ github.repository_owner }}.github.io/verilator/coverage-reports/${{ github.run_id }}/index.html)" >> notification/body.txt + echo "" >> notification/body.txt + echo "Please get to 100% line coverage, and understand all branches; see https://github.com/verilator/verilator/blob/master/docs/internals.rst#code-coverage-results" >> notification/body.txt else echo "Patch contains no code changes" >> notification/body.txt fi diff --git a/docs/internals.rst b/docs/internals.rst index 881885ec2..e970590a4 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -1701,7 +1701,7 @@ can have significant variance. Experience shows that a ~20% time difference can be reliably measured on GitHub hosted runners, and smaller differences are noticeable over a few days of reruns as trends emerge from the noise. -Code coverage +Code Coverage ------------- Code coverage for developing Verilator itself can be collected using @@ -1750,6 +1750,25 @@ example, to see coverage of changes compared to upstream, use: make coverage-view COVERAGE_BASE=origin/master + +Code Coverage Results +--------------------- + +When code coverage is complete, line coverage should be at 100%. Branch +coverage is not required to be at 100%, but all uncovered branches should +be understood as to why. + +For lines which are impossible, use ``// LCOV_EXCL_LINE``, or ``// +LCOV_EXCL_START`` and ``// LCOV_EXCL_STOP`` regions. + +For branches which are impossible, use the ``VL_UNCOVERABLE(condition)`` +macro around uncoverable conditions, otherwise use ``// +LCOV_EXCL_BR_LINE``. + +The assertions ``UASSERT`` and similar are automatically excluded from +coverage, and as such should not require exclusion meta comments. + + Fuzzing ------- From 4befec4463d8e779e619d2b0731db4f723623b9f Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Wed, 29 Apr 2026 23:07:27 +0200 Subject: [PATCH 019/350] Support rand_mode() on static rand class members (#7484) (#7510) --- include/verilated_random.cpp | 8 +- include/verilated_random.h | 7 + src/V3AstAttr.h | 4 + src/V3Randomize.cpp | 215 ++++++++++--- .../t/t_randomize_rand_mode_static.py | 21 ++ test_regress/t/t_randomize_rand_mode_static.v | 296 ++++++++++++++++++ .../t/t_randomize_rand_mode_unsup.out | 24 +- test_regress/t/t_randomize_rand_mode_unsup.v | 4 - 8 files changed, 517 insertions(+), 62 deletions(-) create mode 100755 test_regress/t/t_randomize_rand_mode_static.py create mode 100644 test_regress/t/t_randomize_rand_mode_static.v diff --git a/include/verilated_random.cpp b/include/verilated_random.cpp index 93aba10f9..19817b8d0 100644 --- a/include/verilated_random.cpp +++ b/include/verilated_random.cpp @@ -744,8 +744,12 @@ bool VlRandomizer::parseSolution(std::iostream& os, bool log) { const auto it = m_vars.find(name); if (it == m_vars.end()) continue; const VlRandomVar& varr = *it->second; - if (m_randmodep && !varr.randModeIdxNone()) { - if (!m_randmodep->at(varr.randModeIdx())) continue; + if (!varr.randModeIdxNone()) { + // Static rand vars have their rand_mode in a class-package shared queue, + // not the per-instance one. + const VlQueue* const modep + = m_staticVars.count(name) ? m_static_randmodep : m_randmodep; + if (modep && !modep->at(varr.randModeIdx())) continue; } if (m_disabledVars.count(name)) continue; if (!indices.empty()) { diff --git a/include/verilated_random.h b/include/verilated_random.h index 6ab19a850..e646b6840 100644 --- a/include/verilated_random.h +++ b/include/verilated_random.h @@ -33,6 +33,7 @@ #include #include #include +#include //============================================================================= @@ -219,6 +220,8 @@ class VlRandomizer VL_NOT_FINAL { std::vector m_unique_arrays; std::map m_unique_array_sizes; const VlQueue* m_randmodep = nullptr; // rand_mode state; + const VlQueue* m_static_randmodep = nullptr; // Static rand_mode state (shared) + std::unordered_set m_staticVars; // Names of static rand vars int m_index = 0; // Internal counter for key generation std::set m_randcVarNames; // Names of randc variables for cyclic tracking std::map> @@ -647,6 +650,10 @@ public: void solveBefore(const std::string& beforeName, const std::string& afterName); // Register solve-before ordering void set_randmode(const VlQueue& randmode) { m_randmodep = &randmode; } + // Shared across all instances; consulted instead of m_randmodep for vars marked via + // mark_var_static(). + void set_static_randmode(const VlQueue& randmode) { m_static_randmodep = &randmode; } + void mark_var_static(const char* const name) { m_staticVars.insert(name); } #ifdef VL_DEBUG void dump() const; #endif diff --git a/src/V3AstAttr.h b/src/V3AstAttr.h index 25962098e..69c2f909f 100644 --- a/src/V3AstAttr.h +++ b/src/V3AstAttr.h @@ -834,6 +834,8 @@ public: RANDOMIZER_WRITE_VAR, RANDOMIZER_SET_VAR_DISABLED, RANDOMIZER_CLEAR_VAR_DISABLED, + RANDOMIZER_MARK_VAR_STATIC, + RANDOMIZER_SET_STATIC_RANDMODE, RNG_GET_RANDSTATE, RNG_SET_RANDSTATE, SCHED_ANY_TRIGGERED, @@ -985,6 +987,8 @@ inline std::ostream& operator<<(std::ostream& os, const VCMethod& rhs) { {RANDOMIZER_WRITE_VAR, "write_var", false}, \ {RANDOMIZER_SET_VAR_DISABLED, "set_var_disabled", false}, \ {RANDOMIZER_CLEAR_VAR_DISABLED, "clear_var_disabled", false}, \ + {RANDOMIZER_MARK_VAR_STATIC, "mark_var_static", false}, \ + {RANDOMIZER_SET_STATIC_RANDMODE, "set_static_randmode", false}, \ {RNG_GET_RANDSTATE, "__Vm_rng.get_randstate", true}, \ {RNG_SET_RANDSTATE, "__Vm_rng.set_randstate", false}, \ {SCHED_ANY_TRIGGERED, "anyTriggered", false}, \ diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index e02b9e9fb..d12a3c314 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -469,11 +469,6 @@ class RandomizeMarkVisitor final : public VNVisitor { } else if (nodep->argsp() && !VN_IS(nodep->backp(), StmtExpr)) { nodep->v3error("'rand_mode()' with arguments cannot be called as a function"); valid = false; - } else if (randModeTarget.receiverp - && randModeTarget.receiverp->lifetime().isStatic() - && randModeTarget.receiverp->isRand()) { - nodep->v3warn(E_UNSUPPORTED, "Unsupported: 'rand_mode()' on static variable"); - valid = false; } else if (randModeTarget.receiverp && randModeTarget.receiverp->isRand()) { // Called on a rand member variable RandomizeMode randMode = {}; @@ -483,11 +478,6 @@ class RandomizeMarkVisitor final : public VNVisitor { // Called on 'this' or a non-rand class instance randModeTarget.classp->foreachMember([&](AstClass*, AstVar* varp) { if (!varp->isRand()) return; - if (varp->lifetime().isStatic()) { - nodep->v3warn(E_UNSUPPORTED, - "Unsupported: 'rand_mode()' on static variable: " - << varp->prettyNameQ()); - } RandomizeMode randMode = {}; randMode.usesMode = true; varp->user1(randMode.asInt); @@ -778,6 +768,19 @@ class ConstraintExprVisitor final : public VNVisitor { std::set m_inlineWrittenVars; // Per-instance tracking for inline constraints std::set* m_sizeConstrainedArraysp = nullptr; // Arrays with size+element constraints + // Routes nested sub-objects with static rand vars when the outer class has none. + AstVar* findStaticRandModeVarMember(AstClass* classp) const { + while (true) { + if (AstVar* const varp + = VN_CAST(m_memberMap.findMember(classp, "__Vstaticrandmode"), Var)) { + return varp; + } + AstClassExtends* const extendsp = classp->extendsp(); + if (!extendsp) return nullptr; + classp = extendsp->classp(); + } + } + // Build full path for a MemberSel chain (e.g., "obj.l2.l3.l4") std::string buildMemberPath(const AstMemberSel* const memberSelp) { const AstNode* fromp = memberSelp->fromp(); @@ -1119,9 +1122,19 @@ class ConstraintExprVisitor final : public VNVisitor { AstNodeExpr* constFormatp = membersel ? getConstFormat(membersel->cloneTree(false)) : getConstFormat(nodep); - // Build randmode access: for membersel, use member's class randmode if available + // Static rand vars route through the var's owning class's static array + // (may differ from m_classp when the rand var lives in a sub-object). AstNodeExpr* randModeAccess; - if (membersel) { + const bool varIsStatic = varp->lifetime().isStatic(); + AstClass* const varOwningClassp + = varIsStatic ? VN_CAST(varp->user2p(), Class) : nullptr; + AstVar* const ownerStaticRandModeVarp + = varOwningClassp ? findStaticRandModeVarMember(varOwningClassp) : nullptr; + if (varIsStatic && ownerStaticRandModeVarp) { + randModeAccess = new AstVarRef{ + varp->fileline(), VN_AS(ownerStaticRandModeVarp->user2p(), NodeModule), + ownerStaticRandModeVarp, VAccess::READ}; + } else if (membersel) { AstNodeModule* const varClassp = VN_AS(varp->user2p(), NodeModule); AstVar* const effectiveRandModeVarp = getRandModeVarFromClass(varClassp); if (effectiveRandModeVarp) { @@ -1385,6 +1398,19 @@ class ConstraintExprVisitor final : public VNVisitor { // evaluation), but toggle disabled state so the solver skips // write-back when rand_mode is off. initTaskp->addStmtsp(methodp->makeStmt()); + if (varp->lifetime().isStatic() && randMode.usesMode) { + AstCMethodHard* const markp = new AstCMethodHard{ + varp->fileline(), + new AstVarRef{varp->fileline(), VN_AS(m_genp->user2p(), NodeModule), + m_genp, VAccess::READWRITE}, + VCMethod::RANDOMIZER_MARK_VAR_STATIC}; + AstNodeExpr* const namep = new AstCExpr{varp->fileline(), AstCExpr::Pure{}, + "\"" + smtName + "\"", varp->width()}; + namep->dtypep(varp->dtypep()); + markp->addPinsp(namep); + markp->dtypeSetVoid(); + initTaskp->addStmtsp(markp->makeStmt()); + } if (isGlobalConstrained && membersel && randMode.usesMode) { AstNodeModule* const varClassp = VN_AS(varp->user2p(), NodeModule); AstVar* const subRandModeVarp = getRandModeVarFromClass(varClassp); @@ -3043,6 +3069,7 @@ class RandomizeVisitor final : public VNVisitor { std::map> m_sizeConstrainedArrays; // Per-class arrays std::map m_staticConstraintModeVars; // Static constraint mode vars per class + std::map m_staticRandModeVars; // Static rand mode vars per class // METHODS // Check if two nodes are semantically equivalent (not pointer equality): @@ -3218,6 +3245,22 @@ class RandomizeVisitor final : public VNVisitor { } return nullptr; } + AstVar* getCreateStaticRandModeVar(AstClass* const classp) { + if (m_staticRandModeVars.count(classp)) return m_staticRandModeVars[classp]; + if (AstClassExtends* const extendsp = classp->extendsp()) { + return getCreateStaticRandModeVar(extendsp->classp()); + } + AstVar* const staticModeVarp = createStaticModeVar(classp, "__Vstaticrandmode"); + m_staticRandModeVars[classp] = staticModeVarp; + return staticModeVarp; + } + AstVar* getStaticRandModeVar(AstClass* const classp) { + if (m_staticRandModeVars.count(classp)) return m_staticRandModeVars[classp]; + if (AstClassExtends* const extendsp = classp->extendsp()) { + return getStaticRandModeVar(extendsp->classp()); + } + return nullptr; + } AstVar* createModeVar(AstClass* const classp, const char* const name) { FileLine* const fl = classp->fileline(); if (!m_dynarrayDtp) { @@ -3260,10 +3303,25 @@ class RandomizeVisitor final : public VNVisitor { setRandModep->dtypeSetVoid(); ftaskp->addStmtsp(setRandModep->makeStmt()); } + static void addSetStaticRandMode(AstNodeFTask* const ftaskp, AstVar* const genp, + AstVar* const staticRandModeVarp) { + FileLine* const fl = ftaskp->fileline(); + AstCMethodHard* const setp = new AstCMethodHard{ + fl, new AstVarRef{fl, VN_AS(genp->user2p(), NodeModule), genp, VAccess::WRITE}, + VCMethod::RANDOMIZER_SET_STATIC_RANDMODE, + new AstVarRef{fl, VN_AS(staticRandModeVarp->user2p(), NodeModule), staticRandModeVarp, + VAccess::READ}}; + setp->dtypeSetVoid(); + ftaskp->addStmtsp(setp->makeStmt()); + } void createRandomizeClassVars(AstNetlist* const netlistp) { - netlistp->foreach([this](AstClass* const classp) { + // Defer init to one emission per root with max descendant count; + // super.new() runs Base ctor before Derived can resize the static array. + std::map rootStaticRandModeCount; + netlistp->foreach([this, &rootStaticRandModeCount](AstClass* const classp) { bool hasConstraints = false; uint32_t randModeCount = 0; + uint32_t staticRandModeCount = 0; uint32_t constraintModeCount = 0; uint32_t staticConstraintModeCount = 0; classp->foreachMember([&](AstClass*, AstNode* memberp) { @@ -3288,14 +3346,23 @@ class RandomizeVisitor final : public VNVisitor { constraintModeCount = constraintMode.index + 1; } } - } else if (VN_IS(memberp, Var)) { + } else if (AstVar* const varp = VN_CAST(memberp, Var)) { RandomizeMode randMode = {.asInt = memberp->user1()}; if (!randMode.usesMode) return; + const bool isStaticVar = varp->lifetime().isStatic(); if (randMode.index == 0) { - randMode.index = randModeCount++; + if (isStaticVar) { + randMode.index = staticRandModeCount++; + } else { + randMode.index = randModeCount++; + } memberp->user1(randMode.asInt); } else { - randModeCount = randMode.index + 1; + if (isStaticVar) { + staticRandModeCount = randMode.index + 1; + } else { + randModeCount = randMode.index + 1; + } } } }); @@ -3309,6 +3376,9 @@ class RandomizeVisitor final : public VNVisitor { if (AstVar* const subVarp = VN_CAST(subMemberp, Var)) { const RandomizeMode rm = {.asInt = subVarp->user1()}; if (!rm.usesMode) return; + // Static rand vars index into their own class's static + // rand mode array, not into the outer __Vrandmode. + if (subVarp->lifetime().isStatic()) return; const uint32_t needed = rm.index + 1; if (needed > randModeCount) randModeCount = needed; } @@ -3335,7 +3405,20 @@ class RandomizeVisitor final : public VNVisitor { AstVar* const staticConstraintModeVarp = getCreateStaticConstraintModeVar(classp); makeStaticModeInit(staticConstraintModeVarp, classp, staticConstraintModeCount); } + if (staticRandModeCount > 0) { + getCreateStaticRandModeVar(classp); + AstClass* rootp = classp; + while (AstClassExtends* const ep = rootp->extendsp()) rootp = ep->classp(); + uint32_t& slot = rootStaticRandModeCount[rootp]; + if (staticRandModeCount > slot) slot = staticRandModeCount; + } }); + for (const auto& kv : rootStaticRandModeCount) emitRootStaticModeInit(kv.first, kv.second); + } + void emitRootStaticModeInit(AstClass* const rootp, const uint32_t count) { + AstVar* const staticRandModeVarp = m_staticRandModeVars[rootp]; + UASSERT_OBJ(staticRandModeVarp, rootp, "Root must have a static rand-mode var"); + makeStaticModeInit(staticRandModeVarp, rootp, count); } void makeModeInit(AstVar* modeVarp, AstClass* classp, uint32_t modeCount) { AstNodeModule* const modeVarModp = VN_AS(modeVarp->user2p(), NodeModule); @@ -3423,9 +3506,11 @@ class RandomizeVisitor final : public VNVisitor { new AstAdd{fl, new AstConst{fl, 1}, new AstVarRef{fl, iterVarp, VAccess::READ}}}); return new AstBegin{fl, "", stmtsp, true}; } - static AstNodeStmt* wrapIfRandMode(AstClass* classp, AstVar* const varp, AstNodeStmt* stmtp) { + AstNodeStmt* wrapIfRandMode(AstClass* classp, AstVar* const varp, AstNodeStmt* stmtp) { const RandomizeMode rmode = {.asInt = varp->user1()}; - return VN_AS(wrapIfMode(rmode, getRandModeVarFromClass(classp), stmtp), NodeStmt); + AstVar* const modeVarp = varp->lifetime().isStatic() ? getStaticRandModeVar(classp) + : getRandModeVarFromClass(classp); + return VN_AS(wrapIfMode(rmode, modeVarp, stmtp), NodeStmt); } AstNode* wrapIfConstraintMode(AstClass* classp, AstConstraint* const constrp, AstNode* stmtp) { const RandomizeMode rmode = {.asInt = constrp->user1()}; @@ -3794,17 +3879,23 @@ class RandomizeVisitor final : public VNVisitor { exprp->v3fatalSrc("Not a MemberSel nor VarRef"); return nullptr; // LCOV_EXCL_LINE } - AstNodeExpr* makeSiblingRefp(AstNodeExpr* const exprp, AstVar* const varp, - const VAccess access) { + // Build a reference to a rand_mode/constraint_mode dyn-array. + // Static-mode vars live on the class package; V3Scope resolves the VarRef later. + AstNodeExpr* makeModeVarRef(AstNodeExpr* const exprp, AstVar* const modeVarp, + const VAccess access) { + if (modeVarp->lifetime().isStatic()) { + return new AstVarRef{exprp->fileline(), VN_AS(modeVarp->user2p(), NodeModule), + modeVarp, access}; + } if (AstMemberSel* const memberSelp = VN_CAST(exprp, MemberSel)) { - AstMemberSel* const newMemberSelp - = new AstMemberSel{exprp->fileline(), memberSelp->fromp()->cloneTree(false), varp}; - // Set access on all VarRef nodes in the cloned subtree + AstMemberSel* const newMemberSelp = new AstMemberSel{ + exprp->fileline(), memberSelp->fromp()->cloneTree(false), modeVarp}; newMemberSelp->foreach([access](AstVarRef* varrefp) { varrefp->access(access); }); return newMemberSelp; } UASSERT_OBJ(VN_IS(exprp, VarRef), exprp, "Should be a VarRef"); - return new AstVarRef{exprp->fileline(), VN_AS(varp->user2p(), Class), varp, access}; + return new AstVarRef{exprp->fileline(), VN_AS(modeVarp->user2p(), Class), modeVarp, + access}; } // Get or create a size variable for a constrained dynamic/queue/assoc array. // Returns the size variable. Sets wasCreated=true if a new variable was made. @@ -3850,14 +3941,14 @@ class RandomizeVisitor final : public VNVisitor { storeStmtspr = AstNode::addNext( storeStmtspr, new AstAssign{fl, new AstVarRef{fl, randModeTmpVarp, VAccess::WRITE}, - makeSiblingRefp(siblingExprp, randModeVarp, VAccess::READ)}); + makeModeVarRef(siblingExprp, randModeVarp, VAccess::READ)}); storeStmtspr = AstNode::addNext( storeStmtspr, - makeModeSetLoop(fl, makeSiblingRefp(siblingExprp, randModeVarp, VAccess::WRITE), + makeModeSetLoop(fl, makeModeVarRef(siblingExprp, randModeVarp, VAccess::WRITE), new AstConst{fl, 0}, m_ftaskp)); restoreStmtspr = AstNode::addNext( restoreStmtspr, - new AstAssign{fl, makeSiblingRefp(siblingExprp, randModeVarp, VAccess::WRITE), + new AstAssign{fl, makeModeVarRef(siblingExprp, randModeVarp, VAccess::WRITE), new AstVarRef{fl, randModeTmpVarp, VAccess::READ}}); return randModeTmpVarp; } @@ -3980,9 +4071,10 @@ class RandomizeVisitor final : public VNVisitor { // Generate VarRef with classp as module; V3Scope will update varScopep later // when the variable is moved to the class package. if (modeVarp->lifetime().isStatic()) { - // Static mode var - generate VarRef that will be resolved by V3Scope + // Hint owning class so V3Scope resolves from derived call sites. if (fromp) VL_DO_DANGLING(fromp->unlinkFrBack()->deleteTree(), fromp); - return new AstVarRef{fl, classp, modeVarp, VAccess::WRITE}; + return new AstVarRef{fl, VN_AS(modeVarp->user2p(), NodeModule), modeVarp, + VAccess::WRITE}; } else if (classp == m_modp) { // Called on 'this' or a member of 'this' return new AstVarRef{fl, VN_AS(modeVarp->user2p(), NodeModule), modeVarp, @@ -3996,10 +4088,16 @@ class RandomizeVisitor final : public VNVisitor { // Replace the node with an assignment to the mode variable. Called by visit(AstNodeFTaskRef*) void replaceWithModeAssign(AstNodeFTaskRef* const ftaskRefp, AstNode* const receiverp, AstNodeExpr* const lhsp) { + replaceWithModeAssignAndAppend(ftaskRefp, receiverp, lhsp, nullptr); + } + // Append BEFORE swap; backp()/nextp() unreliable after replaceWith. + void replaceWithModeAssignAndAppend(AstNodeFTaskRef* const ftaskRefp, AstNode* const receiverp, + AstNodeExpr* const lhsp, AstNode* const appendStmtp) { FileLine* const fl = ftaskRefp->fileline(); if (ftaskRefp->argsp()) { UASSERT_OBJ(VN_IS(ftaskRefp->backp(), StmtExpr), ftaskRefp, "Should be a statement"); AstNodeExpr* const rhsp = ftaskRefp->argsp()->exprp()->unlinkFrBack(); + AstNode* newStmtp = nullptr; if (receiverp) { // Called on a rand member variable/constraint. Set the variable/constraint's // mode @@ -4008,16 +4106,19 @@ class RandomizeVisitor final : public VNVisitor { AstCMethodHard* const setp = new AstCMethodHard{fl, lhsp, VCMethod::ARRAY_AT_WRITE, new AstConst{fl, rmode.index}}; setp->dtypeSetUInt32(); - m_stmtp->replaceWith(new AstAssign{fl, setp, rhsp}); + newStmtp = new AstAssign{fl, setp, rhsp}; } else { // For rand_mode: Called on 'this' or a non-rand class instance. // For constraint_mode: Called on a class instance. // Set the rand mode of all members - m_stmtp->replaceWith(makeModeSetLoop(fl, lhsp, rhsp, m_ftaskp)); + newStmtp = makeModeSetLoop(fl, lhsp, rhsp, m_ftaskp); } + if (appendStmtp) newStmtp->addNext(appendStmtp); + m_stmtp->replaceWith(newStmtp); pushDeletep(m_stmtp); } else { UASSERT_OBJ(receiverp, ftaskRefp, "Should have receiver"); + UASSERT_OBJ(!appendStmtp, ftaskRefp, "Append path requires arg-form rand_mode"); const RandomizeMode rmode = {.asInt = receiverp->user1()}; UASSERT_OBJ(rmode.usesMode, ftaskRefp, "Failed to set usesMode"); AstCMethodHard* const setp = new AstCMethodHard{fl, lhsp, VCMethod::ARRAY_AT_WRITE, @@ -4106,7 +4207,11 @@ class RandomizeVisitor final : public VNVisitor { if (commonPrefixp == exprp) break; AstVar* const randVarp = getVarFromRef(exprp); AstClass* const classp = VN_AS(randVarp->user2p(), Class); - AstVar* const randModeVarp = getRandModeVarFromClass(classp); + AstVar* const randModeVarp = randVarp->lifetime().isStatic() + ? getStaticRandModeVar(classp) + : getRandModeVarFromClass(classp); + UASSERT_OBJ(randModeVarp, randVarp, + "Rand var with rand_mode must have a mode array"); if (savedRandModeVarps.find(randModeVarp) == savedRandModeVarps.end()) { AstVar* const randModeTmpVarp = makeTmpRandModeVar(exprp, randModeVarp, storeStmtsp, restoreStmtsp); @@ -4115,7 +4220,7 @@ class RandomizeVisitor final : public VNVisitor { } const RandomizeMode randMode = {.asInt = randVarp->user1()}; AstCMethodHard* setp = new AstCMethodHard{ - fl, makeSiblingRefp(exprp, randModeVarp, VAccess::WRITE), + fl, makeModeVarRef(exprp, randModeVarp, VAccess::WRITE), VCMethod::ARRAY_AT_WRITE, new AstConst{fl, randMode.index}}; setp->dtypeSetUInt32(); setStmtsp @@ -4737,6 +4842,12 @@ class RandomizeVisitor final : public VNVisitor { UASSERT_OBJ(newp, randModeClassp, "No new() in class"); addSetRandMode(newp, genp, randModeVarp); } + if (AstVar* const staticRandModeVarp = getStaticRandModeVar(nodep)) { + // Wire the shared static rand_mode queue into the class generator. + AstNodeFTask* const newp = VN_AS(m_memberMap.findMember(nodep, "new"), NodeFTask); + UASSERT_OBJ(newp, nodep, "No new() in class"); + addSetStaticRandMode(newp, genp, staticRandModeVarp); + } } else { beginValp = new AstConst{fl, AstConst::WidthedValue{}, 32, 1}; } @@ -4842,13 +4953,36 @@ class RandomizeVisitor final : public VNVisitor { UASSERT_OBJ(randModeTarget.classp, nodep, "Should have checked in RandomizeMarkVisitor"); AstVar* const receiverp = randModeTarget.receiverp; - AstVar* const randModeVarp = getRandModeVarFromClass(randModeTarget.classp); + const bool isClassLevel = !(receiverp && receiverp->rand().isRand()); + // Class-level rand_mode(N) must also flush the shared static array. + AstNode* classLevelStaticLoopp = nullptr; + if (isClassLevel && nodep->argsp()) { + if (AstVar* const sVarp = getStaticRandModeVar(randModeTarget.classp)) { + FileLine* const fl = nodep->fileline(); + AstNodeExpr* const staticLhsp + = makeModeAssignLhs(fl, randModeTarget.classp, nullptr, sVarp); + AstNodeExpr* const argClonep = nodep->argsp()->exprp()->cloneTreePure(false); + classLevelStaticLoopp = makeModeSetLoop(fl, staticLhsp, argClonep, m_ftaskp); + } + } + const bool receiverIsStaticRand + = receiverp && receiverp->rand().isRand() && receiverp->lifetime().isStatic(); + AstVar* const randModeVarp = receiverIsStaticRand + ? getStaticRandModeVar(randModeTarget.classp) + : getRandModeVarFromClass(randModeTarget.classp); + if (!randModeVarp) { + UASSERT_OBJ(isClassLevel && classLevelStaticLoopp, nodep, + "Per-instance rand_mode var missing without static fallback"); + UASSERT_OBJ(VN_IS(nodep->backp(), StmtExpr), nodep, "Should be a statement"); + m_stmtp->replaceWith(classLevelStaticLoopp); + pushDeletep(m_stmtp); + return; + } AstNodeExpr* const lhsp = makeModeAssignLhs(nodep->fileline(), randModeTarget.classp, randModeTarget.fromp, randModeVarp); - replaceWithModeAssign(nodep, - // If the receiver is not rand, set the rand_mode for all members - receiverp && receiverp->rand().isRand() ? receiverp : nullptr, - lhsp); + replaceWithModeAssignAndAppend( + nodep, receiverp && receiverp->rand().isRand() ? receiverp : nullptr, lhsp, + classLevelStaticLoopp); return; } @@ -5092,6 +5226,11 @@ class RandomizeVisitor final : public VNVisitor { // Set rand mode if present (not needed if classGenp exists and was copied) AstVar* const randModeVarp = getRandModeVarFromClass(classp); if (!classGenp && randModeVarp) addSetRandMode(randomizeFuncp, localGenp, randModeVarp); + if (!classGenp) { + if (AstVar* const sVarp = getStaticRandModeVar(classp)) { + addSetStaticRandMode(randomizeFuncp, localGenp, sVarp); + } + } // Generate constraint setup code and a hardcoded call to the solver AstNode* const capturedTreep = withp->exprp()->unlinkFrBackWithNext(); diff --git a/test_regress/t/t_randomize_rand_mode_static.py b/test_regress/t/t_randomize_rand_mode_static.py new file mode 100755 index 000000000..db1adb3f9 --- /dev/null +++ b/test_regress/t/t_randomize_rand_mode_static.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +if not test.have_solver: + test.skip("No constraint solver installed") + +test.compile() + +test.execute() + +test.passes() diff --git a/test_regress/t/t_randomize_rand_mode_static.v b/test_regress/t/t_randomize_rand_mode_static.v new file mode 100644 index 000000000..ba479b3a0 --- /dev/null +++ b/test_regress/t/t_randomize_rand_mode_static.v @@ -0,0 +1,296 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +`define check_range(gotv,minv,maxv) do if ((gotv) < (minv) || (gotv) > (maxv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d-%0d\n", `__FILE__,`__LINE__, (gotv), (minv), (maxv)); `stop; end while(0); +// verilog_format: on + +// Tests rand_mode() support for `static rand` class members per IEEE 1800-2023 +// Section 18.5.10 and Section 18.4. Static rand_mode state is shared across all +// instances of a class. + +class Simple; + static rand int sx; + rand int dy; + constraint c { + sx > 0; + sx < 12; + dy > 100; + dy < 200; + } +endclass + +class Base; + static rand bit [3:0] base_sx; + rand bit [3:0] base_dy; + constraint base_c { + base_sx > 0; + base_dy > 0; + } +endclass + +class Derived extends Base; + rand bit [3:0] der_dy; + constraint der_c {der_dy > 0;} +endclass + +// Class with ONLY static rand members; exercises class-level rand_mode() with +// no per-instance mode array. +class StaticOnly; + static rand bit [3:0] sa; + static rand bit [3:0] sb; + constraint c { + sa > 0; + sb > 0; + } +endclass + +// Base with two static rand vars exercises non-zero index in the shared static array. +class BaseTwo; + static rand bit [3:0] base2_a; + static rand bit [3:0] base2_b; + rand bit [3:0] base2_dy; + constraint c { + base2_a > 0; + base2_b > 0; + base2_dy > 0; + } +endclass + +class DerivedTwo extends BaseTwo; + rand bit [3:0] der2_dy; + constraint dc {der2_dy > 0;} +endclass + +// No constraint blocks: inline randomize-with must still flush static rand_mode. +class StaticNoConstraint; + static rand bit [3:0] snc_s; + rand bit [3:0] snc_d; +endclass + +// Base + Derived each declare a static rand var; per-root max-count init must size for both. +class BaseS; + static rand bit [3:0] base_s; + constraint c {base_s > 0;} +endclass + +class DerivedS extends BaseS; + static rand bit [3:0] der_s; + constraint c2 {der_s > 0;} +endclass + +module t; + Simple s1, s2; + Derived d1, d2; + StaticOnly so1, so2; + BaseS bs1; + DerivedS ds1, ds2; + DerivedTwo dt1; + StaticNoConstraint snc1; + int saved_sx, saved_dy, rok; + bit [3:0] saved_base_sx, saved_base_s, saved_der_s, saved_base2_b; + + initial begin + s1 = new; + s2 = new; + + // ---- Test 1: getter on static rand var (initial state is enabled) + `checkd(s1.sx.rand_mode(), 1); + `checkd(s2.sx.rand_mode(), 1); + + // ---- Test 2: randomize() with all rand_modes enabled satisfies constraints + repeat (20) begin + rok = s1.randomize(); + `checkd(rok, 1); + `check_range(Simple::sx, 1, 11); + `check_range(s1.dy, 101, 199); + end + + // ---- Test 3: per-member set on a static rand var is shared across instances + s1.sx.rand_mode(0); + `checkd(s1.sx.rand_mode(), 0); + `checkd(s2.sx.rand_mode(), 0); + + // Non-static dy stays independent on each instance + `checkd(s1.dy.rand_mode(), 1); + `checkd(s2.dy.rand_mode(), 1); + + // ---- Test 4: solver respects rand_mode(0) on static rand var. + // sx is currently a valid value from Test 2; with rand_mode disabled it + // must stay at that value across multiple randomize() calls. + saved_sx = Simple::sx; + repeat (20) begin + rok = s1.randomize(); + `checkd(rok, 1); + `checkd(Simple::sx, saved_sx); + `check_range(s1.dy, 101, 199); + end + + // Re-enable rand_mode for sx via the OTHER instance (sharing test). + s2.sx.rand_mode(1); + `checkd(s1.sx.rand_mode(), 1); + + repeat (20) begin + rok = s1.randomize(); + `checkd(rok, 1); + `check_range(Simple::sx, 1, 11); + `check_range(s1.dy, 101, 199); + end + + // ---- Test 5: class-level obj.rand_mode(0) flushes both per-instance + // and static arrays. Disable everything on s1. + s1.rand_mode(0); + `checkd(s1.sx.rand_mode(), 0); + `checkd(s1.dy.rand_mode(), 0); + // Static sx is shared, so s2 sees it disabled too. + `checkd(s2.sx.rand_mode(), 0); + // Non-static dy on s2 is independent of s1's class-level call. + `checkd(s2.dy.rand_mode(), 1); + + // Re-randomize s1 with everything off - both fields unchanged. + saved_sx = Simple::sx; + saved_dy = s1.dy; + repeat (10) begin + rok = s1.randomize(); + `checkd(rok, 1); + `checkd(Simple::sx, saved_sx); + `checkd(s1.dy, saved_dy); + end + + // Class-level enable resets both arrays to 1. + s1.rand_mode(1); + `checkd(s1.sx.rand_mode(), 1); + `checkd(s1.dy.rand_mode(), 1); + `checkd(s2.sx.rand_mode(), 1); + + // ---- Test 6: inheritance - static rand var declared in base class, + // accessed via a derived-class instance. + d1 = new; + d2 = new; + + `checkd(d1.base_sx.rand_mode(), 1); + + // Randomize first so base_sx satisfies its constraint, then disable it. + rok = d1.randomize(); + `checkd(rok, 1); + if (Base::base_sx == 0) $stop; + + d1.base_sx.rand_mode(0); + `checkd(d1.base_sx.rand_mode(), 0); + `checkd(d2.base_sx.rand_mode(), 0); // Shared via base class + + // Derived class member dy is non-static, independent. + `checkd(d1.der_dy.rand_mode(), 1); + `checkd(d2.der_dy.rand_mode(), 1); + + saved_base_sx = Base::base_sx; + repeat (20) begin + rok = d1.randomize(); + `checkd(rok, 1); + `checkd(Base::base_sx, saved_base_sx); // disabled - unchanged + if (d1.der_dy == 0) $stop; + if (d1.base_dy == 0) $stop; + end + + // ---- Test 7: class-level rand_mode(N) on a class with ONLY static rand members. + so1 = new; + so2 = new; + `checkd(so1.sa.rand_mode(), 1); + `checkd(so1.sb.rand_mode(), 1); + so1.rand_mode(0); // must not crash + `checkd(so1.sa.rand_mode(), 0); + `checkd(so1.sb.rand_mode(), 0); + `checkd(so2.sa.rand_mode(), 0); // shared + `checkd(so2.sb.rand_mode(), 0); // shared + so2.rand_mode(1); + `checkd(so1.sa.rand_mode(), 1); + `checkd(so1.sb.rand_mode(), 1); + + // ---- Test 8: inline obj.randomize(static_var) save/restore. + // The inline form must route through the static rand_mode array, not + // the per-instance one (whose index space is different / smaller). + s1.sx.rand_mode(1); + s1.dy.rand_mode(1); + repeat (10) begin + rok = s1.randomize(sx); // only sx is randomized, dy frozen + `checkd(rok, 1); + `check_range(Simple::sx, 1, 11); + end + // After the inline call, s1.sx.rand_mode() must be back to 1 + // (the save/restore restores the static array). + `checkd(s1.sx.rand_mode(), 1); + `checkd(s2.sx.rand_mode(), 1); // shared - also 1 + + // ---- Test 9: Base AND Derived each declare own static rand var. + // Derived's static array must be sized to fit BOTH base_s and der_s + // even though super.new() runs Base's init first. + ds1 = new; + ds2 = new; + `checkd(ds1.base_s.rand_mode(), 1); + `checkd(ds1.der_s.rand_mode(), 1); + rok = ds1.randomize(); + `checkd(rok, 1); + if (BaseS::base_s == 0) $stop; + if (DerivedS::der_s == 0) $stop; + + // Disable both via per-member call + ds1.base_s.rand_mode(0); + ds1.der_s.rand_mode(0); + `checkd(ds2.base_s.rand_mode(), 0); // shared + `checkd(ds2.der_s.rand_mode(), 0); // shared + saved_base_s = BaseS::base_s; + saved_der_s = DerivedS::der_s; + repeat (10) begin + rok = ds1.randomize(); + `checkd(rok, 1); + `checkd(BaseS::base_s, saved_base_s); + `checkd(DerivedS::der_s, saved_der_s); + end + + // Construct a standalone BaseS AFTER DerivedS already initialized the + // static array; BaseS init must see size != 0 and skip without + // overwriting Derived's prior rand_mode(0) state. + bs1 = new; + `checkd(bs1.base_s.rand_mode(), 0); // still disabled + + // ---- Test 10: two static rand vars in Base; Derived must accumulate inherited indices. + dt1 = new; + `checkd(dt1.base2_a.rand_mode(), 1); + `checkd(dt1.base2_b.rand_mode(), 1); + rok = dt1.randomize(); + `checkd(rok, 1); + if (BaseTwo::base2_a == 0) $stop; + if (BaseTwo::base2_b == 0) $stop; + // Disable second static var to prove its index is reachable in the array. + dt1.base2_b.rand_mode(0); + `checkd(dt1.base2_b.rand_mode(), 0); + `checkd(dt1.base2_a.rand_mode(), 1); // first still enabled + saved_base2_b = BaseTwo::base2_b; + repeat (10) begin + rok = dt1.randomize(); + `checkd(rok, 1); + `checkd(BaseTwo::base2_b, saved_base2_b); // disabled - unchanged + if (BaseTwo::base2_a == 0) $stop; // still randomizing + end + + // ---- Test 11: inline randomize-with on class with static rand and no class-level constraints. + snc1 = new; + snc1.snc_s.rand_mode(1); // ensure static rand-mode array exists + repeat (10) begin + rok = snc1.randomize() with { + snc_d > 5; + snc_d < 13; + }; + `checkd(rok, 1); + `check_range(snc1.snc_d, 6, 12); + end + + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_randomize_rand_mode_unsup.out b/test_regress/t/t_randomize_rand_mode_unsup.out index e894388b8..900a92d4e 100644 --- a/test_regress/t/t_randomize_rand_mode_unsup.out +++ b/test_regress/t/t_randomize_rand_mode_unsup.out @@ -1,26 +1,14 @@ -%Error-UNSUPPORTED: t/t_randomize_rand_mode_unsup.v:17:20: Unsupported: 'rand_mode()' on dynamic array element +%Error-UNSUPPORTED: t/t_randomize_rand_mode_unsup.v:16:20: Unsupported: 'rand_mode()' on dynamic array element : ... note: In instance 't' - 17 | p.m_dyn_arr[0].rand_mode(0); + 16 | p.m_dyn_arr[0].rand_mode(0); | ^~~~~~~~~ ... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest -%Error-UNSUPPORTED: t/t_randomize_rand_mode_unsup.v:18:20: Unsupported: 'rand_mode()' on unpacked array element +%Error-UNSUPPORTED: t/t_randomize_rand_mode_unsup.v:17:20: Unsupported: 'rand_mode()' on unpacked array element : ... note: In instance 't' - 18 | p.m_unp_arr[0].rand_mode(0); + 17 | p.m_unp_arr[0].rand_mode(0); | ^~~~~~~~~ -%Error-UNSUPPORTED: t/t_randomize_rand_mode_unsup.v:19:18: Unsupported: 'rand_mode()' on unpacked struct element +%Error-UNSUPPORTED: t/t_randomize_rand_mode_unsup.v:18:18: Unsupported: 'rand_mode()' on unpacked struct element : ... note: In instance 't' - 19 | p.m_struct.y.rand_mode(0); + 18 | p.m_struct.y.rand_mode(0); | ^~~~~~~~~ -%Error-UNSUPPORTED: t/t_randomize_rand_mode_unsup.v:20:16: Unsupported: 'rand_mode()' on static variable - : ... note: In instance 't' - 20 | p.m_static.rand_mode(0); - | ^~~~~~~~~ -%Error-UNSUPPORTED: t/t_randomize_rand_mode_unsup.v:21:55: Unsupported: 'rand_mode()' on static variable - : ... note: In instance 't' - 21 | $display("p.m_static.rand_mode()=%0d", p.m_static.rand_mode()); - | ^~~~~~~~~ -%Error-UNSUPPORTED: t/t_randomize_rand_mode_unsup.v:22:7: Unsupported: 'rand_mode()' on static variable: 'm_static' - : ... note: In instance 't' - 22 | p.rand_mode(0); - | ^~~~~~~~~ %Error: Exiting due to diff --git a/test_regress/t/t_randomize_rand_mode_unsup.v b/test_regress/t/t_randomize_rand_mode_unsup.v index 0e66b1c98..df4a88275 100644 --- a/test_regress/t/t_randomize_rand_mode_unsup.v +++ b/test_regress/t/t_randomize_rand_mode_unsup.v @@ -8,7 +8,6 @@ class Packet; rand int m_dyn_arr[]; rand int m_unp_arr[10]; rand struct {int y;} m_struct; - static rand int m_static; endclass module t; @@ -17,8 +16,5 @@ module t; p.m_dyn_arr[0].rand_mode(0); p.m_unp_arr[0].rand_mode(0); p.m_struct.y.rand_mode(0); - p.m_static.rand_mode(0); - $display("p.m_static.rand_mode()=%0d", p.m_static.rand_mode()); - p.rand_mode(0); end endmodule From d15b1fba9467e6423d091295e5dc0feb61124f4a Mon Sep 17 00:00:00 2001 From: em2machine <92717390+em2machine@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:12:11 -0400 Subject: [PATCH 020/350] Fix $bits on local struct with chained-interface (#7515) (#7517) --- src/V3Param.cpp | 12 ++++ .../t/t_iface_chained_consumer_struct.py | 18 ++++++ .../t/t_iface_chained_consumer_struct.v | 57 +++++++++++++++++++ .../t/t_paramgraph_comined_iface_stats.py | 2 +- ..._paramgraph_iface_template_nested_stats.py | 2 +- ...t_paramgraph_nested_iface_typedef_stats.py | 2 +- 6 files changed, 90 insertions(+), 3 deletions(-) create mode 100755 test_regress/t/t_iface_chained_consumer_struct.py create mode 100644 test_regress/t/t_iface_chained_consumer_struct.v diff --git a/src/V3Param.cpp b/src/V3Param.cpp index 13ead637a..51790b74d 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -875,6 +875,18 @@ class ParamProcessor final { } return true; } + } else if (!entry.cloneCellPath.empty()) { + // Clone entry has no paramTypep stored; look up the type by name. + if (AstParamTypeDType* const ptp + = V3LinkDotIfaceCapture::findParamTypeInModule(targetModp, entry.refp->name())) { + entry.refp->refDTypep(ptp); + entry.refp->dtypep(ptp); + for (AstRefDType* const xrefp : entry.extraRefps) { + xrefp->refDTypep(ptp); + xrefp->dtypep(ptp); + } + return true; + } } return false; } diff --git a/test_regress/t/t_iface_chained_consumer_struct.py b/test_regress/t/t_iface_chained_consumer_struct.py new file mode 100755 index 000000000..6fe7d000c --- /dev/null +++ b/test_regress/t/t_iface_chained_consumer_struct.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=["--binary"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_iface_chained_consumer_struct.v b/test_regress/t/t_iface_chained_consumer_struct.v new file mode 100644 index 000000000..0c0305b4e --- /dev/null +++ b/test_regress/t/t_iface_chained_consumer_struct.v @@ -0,0 +1,57 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// Two-level interface chain. Inner interface has a typedef that +// depends on its parameter. Mid interface aliases that typedef. +// A module aliases the alias and uses it in a packed struct, then +// passes $bits(struct) to a width-parameterized child. All widths +// must use the override value, not the template default. + +interface inner_if #(parameter int N = 1) (); + typedef logic [$clog2(N)-1:0] id_t; +endinterface + +interface mid_if #(parameter int N = 1) (); + inner_if #(.N(N)) inner(); + typedef inner.id_t id_t; +endinterface + +module sink #(parameter int W = 1) (input logic [W-1:0] dat_i); +endmodule + +module dut #(parameter int N = 1) (); + mid_if #(.N(N)) m(); + typedef m.id_t id_t; + typedef struct packed { + id_t id; + logic [7:0] payload; + } pkt_t; + pkt_t pkt_var; + localparam int W = $bits(pkt_t); + sink #(.W(W)) s(.dat_i(pkt_var)); +endmodule + +module t; + // N=8 gives id_t = 3 bits, so pkt_t = 3 + 8 = 11 bits. + dut #(.N(8)) u(); + + initial begin + if (u.W !== 11) begin + $display("%%Error: u.W=%0d expected 11", u.W); + $stop; + end + if ($bits(u.pkt_var) !== 11) begin + $display("%%Error: $bits(u.pkt_var)=%0d expected 11", $bits(u.pkt_var)); + $stop; + end + if ($bits(u.s.dat_i) !== 11) begin + $display("%%Error: $bits(u.s.dat_i)=%0d expected 11", $bits(u.s.dat_i)); + $stop; + end + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_paramgraph_comined_iface_stats.py b/test_regress/t/t_paramgraph_comined_iface_stats.py index 689830310..4732b2b49 100755 --- a/test_regress/t/t_paramgraph_comined_iface_stats.py +++ b/test_regress/t/t_paramgraph_comined_iface_stats.py @@ -23,7 +23,7 @@ test.file_grep(test.stats, r'IfaceCapture, Entries template\s+(\d+)', 8) test.file_grep(test.stats, r'IfaceCapture, Entries cloned\s+(\d+)', 10) test.file_grep(test.stats, r'IfaceCapture, Ledger fixups in V3Param\s+(\d+)', 8) test.file_grep(test.stats, r'IfaceCapture, Wrong-clone refs fixed\s+(\d+)', 10) -test.file_grep(test.stats, r'IfaceCapture, Dead refs fixed in modules\s+(\d+)', 2) +test.file_grep(test.stats, r'IfaceCapture, Dead refs fixed in modules\s+(\d+)', 0) test.execute() diff --git a/test_regress/t/t_paramgraph_iface_template_nested_stats.py b/test_regress/t/t_paramgraph_iface_template_nested_stats.py index 5ba344e1f..f6e57844b 100755 --- a/test_regress/t/t_paramgraph_iface_template_nested_stats.py +++ b/test_regress/t/t_paramgraph_iface_template_nested_stats.py @@ -23,7 +23,7 @@ test.file_grep(test.stats, r'IfaceCapture, Entries template\s+(\d+)', 11) test.file_grep(test.stats, r'IfaceCapture, Entries cloned\s+(\d+)', 14) test.file_grep(test.stats, r'IfaceCapture, Ledger fixups in V3Param\s+(\d+)', 5) test.file_grep(test.stats, r'IfaceCapture, Wrong-clone refs fixed\s+(\d+)', 10) -test.file_grep(test.stats, r'IfaceCapture, Dead refs fixed in modules\s+(\d+)', 2) +test.file_grep(test.stats, r'IfaceCapture, Dead refs fixed in modules\s+(\d+)', 0) test.execute() diff --git a/test_regress/t/t_paramgraph_nested_iface_typedef_stats.py b/test_regress/t/t_paramgraph_nested_iface_typedef_stats.py index 841c59c88..e0142486f 100755 --- a/test_regress/t/t_paramgraph_nested_iface_typedef_stats.py +++ b/test_regress/t/t_paramgraph_nested_iface_typedef_stats.py @@ -23,7 +23,7 @@ test.file_grep(test.stats, r'IfaceCapture, Entries template\s+(\d+)', 8) test.file_grep(test.stats, r'IfaceCapture, Entries cloned\s+(\d+)', 12) test.file_grep(test.stats, r'IfaceCapture, Ledger fixups in V3Param\s+(\d+)', 8) test.file_grep(test.stats, r'IfaceCapture, Wrong-clone refs fixed\s+(\d+)', 14) -test.file_grep(test.stats, r'IfaceCapture, Dead refs fixed in modules\s+(\d+)', 4) +test.file_grep(test.stats, r'IfaceCapture, Dead refs fixed in modules\s+(\d+)', 0) test.execute() From 21020ea2d1a757608a9081f8055c3296e42b470e Mon Sep 17 00:00:00 2001 From: Nick Brereton <85175726+nbstrike@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:15:38 -0400 Subject: [PATCH 021/350] Support printing enum names for %p and %s (#5523) (#7338) --- include/verilated.cpp | 48 +++++-- include/verilatedos.h | 1 + src/V3EmitCFunc.cpp | 2 +- src/V3Number.h | 2 + src/V3Width.cpp | 57 +++++++- test_regress/t/t_class_enum.v | 26 ++++ test_regress/t/t_display_enum_format.py | 18 +++ test_regress/t/t_display_enum_format.v | 167 ++++++++++++++++++++++++ test_regress/t/t_enum_huge_methods.v | 8 +- test_regress/t/t_enum_large_methods.v | 6 +- test_regress/t/t_enum_type_methods.v | 52 +++++--- 11 files changed, 356 insertions(+), 31 deletions(-) create mode 100755 test_regress/t/t_display_enum_format.py create mode 100644 test_regress/t/t_display_enum_format.v diff --git a/include/verilated.cpp b/include/verilated.cpp index 3e20be69d..942d2877f 100644 --- a/include/verilated.cpp +++ b/include/verilated.cpp @@ -944,6 +944,20 @@ std::string _vl_vsformat_time(std::string& tmp, T ld, int timeunit, bool left, // Do a va_arg returning a quad, assuming input argument is anything less than wide #define VL_VA_ARG_Q_(ap, bits) (((bits) <= VL_IDATASIZE) ? va_arg(ap, IData) : va_arg(ap, QData)) +static void _vl_vsformat_read_qint(va_list app, int lbits, QData& ld, std::vector& strwide, + WDataInP& lwp, int& lsb) VL_MT_SAFE { + if (lbits <= VL_QUADSIZE) { + ld = VL_VA_ARG_Q_(app, lbits); + strwide.resize(2); + VL_SET_WQ(strwide, ld); + lwp = strwide.data(); + } else { + lwp = va_arg(app, WDataInP); + ld = 0; // Consume the arg; enums > 64 bits wide are unsupported. + } + lsb = lbits - 1; +} + void _vl_vsformat(std::string& output, const std::string& format, int argc, va_list ap) VL_MT_SAFE { // Format a Verilog $write style format into the output list @@ -1078,6 +1092,7 @@ void _vl_vsformat(std::string& output, const std::string& format, int argc, // Similar code flow in V3Number::displayed int lbits = 0; void* thingp = nullptr; + const std::string* enump = nullptr; QData ld = 0; std::vector strwide; WDataInP lwp = nullptr; @@ -1097,17 +1112,31 @@ void _vl_vsformat(std::string& output, const std::string& format, int argc, } else if (formatAttr == VL_VFORMATATTR_STRING) { thingp = va_arg(ap, std::string*); if (fmt != 'p' && fmt != 'x') fmt = 's'; // Override + } else if (formatAttr == VL_VFORMATATTR_ENUM) { + lbits = va_arg(ap, int); + _vl_vsformat_read_qint(ap, lbits, ld, strwide, lwp, lsb); + ++argn; // Internal ABI: runtime enum args are followed by generated name string + static_cast(va_arg(ap, int)); // VL_VFORMATATTR_STRING + enump = va_arg(ap, std::string*); + if (enump && !enump->empty()) { + formatAttr = (fmt == 'p') ? VL_VFORMATATTR_COMPLEX : VL_VFORMATATTR_STRING; + thingp = const_cast(enump); + } else if (fmt == 'p' && widthSet && width == 0) { + output += "'h"; + fmt = 'h'; + formatAttr = VL_VFORMATATTR_UNSIGNED; + } else { + if (fmt == 'p') width = 0; + widthSet = true; + fmt = 'd'; + formatAttr = VL_VFORMATATTR_UNSIGNED; + } + if (widthSet && width == 0) { + while (lsb && !VL_BITISSET_W(lwp, lsb)) --lsb; + } } else { // Numeric lbits = va_arg(ap, int); - if (lbits <= VL_QUADSIZE) { - ld = VL_VA_ARG_Q_(ap, lbits); - strwide.resize(2); - VL_SET_WQ(strwide, ld); - lwp = strwide.data(); - } else { - lwp = va_arg(ap, WDataInP); - ld = lwp[0]; - } + _vl_vsformat_read_qint(ap, lbits, ld, strwide, lwp, lsb); if (fmt == 'p') { if (widthSet && width == 0) { // For %0p, IEEE our choice, use 'h%0h output += "'h"; @@ -1118,7 +1147,6 @@ void _vl_vsformat(std::string& output, const std::string& format, int argc, fmt = 'd'; } } - lsb = lbits - 1; if (widthSet && width == 0) { while (lsb && !VL_BITISSET_W(lwp, lsb)) --lsb; } diff --git a/include/verilatedos.h b/include/verilatedos.h index b93909309..3db48823a 100644 --- a/include/verilatedos.h +++ b/include/verilatedos.h @@ -442,6 +442,7 @@ using ssize_t = uint32_t; ///< signed size_t; returned from read() #define VL_VFORMATATTR_SIGNED '~' // (int widthMin, IData/WData/etc) Signed number; for %d showing sign #define VL_VFORMATATTR_COMPLEX '!' // (std::string*); for non-POD; e.g. struct, requires %p typically #define VL_VFORMATATTR_DOUBLE 'D' // (double); promote %p to %f +#define VL_VFORMATATTR_ENUM 'E' // (width, data..., std::string* name); enum with runtime %p/%s #define VL_VFORMATATTR_SCOPE 'M' // (char* name, char* scope); for scopes #define VL_VFORMATATTR_STRING 'S' // (char* name, char* scope); for scopes // (std::string*); for %p/%s #define VL_VFORMATATTR_TIMEUNIT 'T' // (int timeunit); timeunits passed from V3Emit to runtime diff --git a/src/V3EmitCFunc.cpp b/src/V3EmitCFunc.cpp index f324623a0..a18e09deb 100644 --- a/src/V3EmitCFunc.cpp +++ b/src/V3EmitCFunc.cpp @@ -318,7 +318,7 @@ void EmitCFunc::displayNode(AstNode* nodep, AstSFormatF* fmtp, // fmtp is nullp AstNode* const subargp = fargp ? fargp->exprp() : argp; const VFormatAttr formatAttr = AstSFormatArg::formatAttrDefauled(fargp, subargp->dtypep()); puts(", '"s + formatAttr.ascii() + '\''); - if (formatAttr.isSigned() || formatAttr.isUnsigned()) + if (formatAttr.isSigned() || formatAttr.isUnsigned() || formatAttr.isEnum()) puts("," + cvtToStr(subargp->widthMin())); const bool addrof = isScan || formatAttr.isString() || formatAttr.isComplex(); puts(","); diff --git a/src/V3Number.h b/src/V3Number.h index c5d1a57bf..031554a75 100644 --- a/src/V3Number.h +++ b/src/V3Number.h @@ -46,6 +46,7 @@ public: // COMPLEX = VL_VFORMATATTR_COMPLEX, DOUBLE = VL_VFORMATATTR_DOUBLE, + ENUM = VL_VFORMATATTR_ENUM, SCOPE = VL_VFORMATATTR_SCOPE, STRING = VL_VFORMATATTR_STRING, TIMEUNIT = VL_VFORMATATTR_TIMEUNIT @@ -62,6 +63,7 @@ public: char ascii() const { return m_e; } bool isComplex() const { return m_e == COMPLEX; } bool isDouble() const { return m_e == DOUBLE; } + bool isEnum() const { return m_e == ENUM; } bool isSigned() const { return m_e == SIGNED; } bool isString() const { return m_e == STRING; } bool isUnsigned() const { return m_e == UNSIGNED; } diff --git a/src/V3Width.cpp b/src/V3Width.cpp index 713388321..6cb6959bd 100644 --- a/src/V3Width.cpp +++ b/src/V3Width.cpp @@ -6364,7 +6364,17 @@ class WidthVisitor final : public VNVisitor { AstNodeExpr* const newp = new AstToStringN{argp->fileline(), argp}; formatAttr = VFormatAttr::COMPLEX; argp = newp; - } else if (dtypep->isSigned()) { + } else if (nodep->exprFormat()) { + if (AstEnumDType* const enumDtp = formatEnumDType(argp)) { + nodep->addExprsp(new AstSFormatArg{argp->fileline(), VFormatAttr::ENUM, argp}); + AstNodeExpr* const namep + = enumSelect(argp->cloneTreePure(false), enumDtp, VAttrType::ENUM_NAME); + nodep->addExprsp( + new AstSFormatArg{namep->fileline(), VFormatAttr::STRING, namep}); + continue; + } + } + if (formatAttr.isUnsigned() && dtypep->isSigned()) { formatAttr = VFormatAttr::SIGNED; } if (VN_IS(argp, SFormatArg) // Already done @@ -8231,13 +8241,16 @@ class WidthVisitor final : public VNVisitor { // For sformatf's with constant format, iterate/check arguments UASSERT_OBJ(!nodep->exprFormat(), nodep, "Assumes constant format"); bool inPct = false; + string fmtMods; AstNodeExpr* argp = nodep->exprsp(); string newFormat; for (char ch : nodep->text()) { if (!inPct && ch == '%') { inPct = true; + fmtMods = ""; newFormat += ch; } else if (inPct && (std::isdigit(ch) || ch == '.' || ch == '-')) { + fmtMods += ch; newFormat += ch; } else if (!inPct) { // Normal text newFormat += ch; @@ -8245,7 +8258,7 @@ class WidthVisitor final : public VNVisitor { inPct = false; AstNodeExpr* const nextp = argp ? VN_AS(argp->nextp(), NodeExpr) : nullptr; AstSFormatArg* const fargp = VN_CAST(argp, SFormatArg); // May not exist yet - AstNodeExpr* const subargp = fargp ? fargp->exprp() : argp; + AstNodeExpr* subargp = fargp ? fargp->exprp() : argp; const AstNodeDType* const dtypep = subargp ? subargp->dtypep()->skipRefp() : nullptr; ch = std::tolower(ch); @@ -8302,7 +8315,35 @@ class WidthVisitor final : public VNVisitor { } break; case 'p': // FALLTHRU - case 's': // FALLTHRU + case 's': + // Keep enum `%p`/`%s` behavior aligned with enum.name(): + // valid enum values print the mnemonic; invalid values print numeric fallback. + if (subargp) { + if (AstEnumDType* const enumDtp = formatEnumDType(subargp)) { + string fallbackFormat = "%0d"; + if (ch == 'p') { + bool widthSet = false; + size_t width = 0; + for (const char mod : fmtMods) { + if (!std::isdigit(mod)) continue; + widthSet = true; + width = width * 10 + (mod - '0'); + } + if (widthSet && width == 0) fallbackFormat = "'h%0h"; + } + AstNodeExpr* const newp = new AstCond{ + subargp->fileline(), enumTestValid(subargp, enumDtp), + enumSelect(subargp->cloneTreePure(false), enumDtp, + VAttrType::ENUM_NAME), + new AstSFormatF{subargp->fileline(), fallbackFormat, true, + subargp->cloneTreePure(false)}}; + subargp->replaceWith(new AstSFormatArg{subargp->fileline(), + VFormatAttr::COMPLEX, newp}); + VL_DO_DANGLING(pushDeletep(subargp), subargp); + } + } + argp = nextp; + break; default: // Most operators, just move to next argument argp = nextp; break; @@ -8313,6 +8354,16 @@ class WidthVisitor final : public VNVisitor { nodep->text(newFormat); } + static AstEnumDType* formatEnumDType(AstNodeExpr* subargp) { + AstEnumDType* enumDtp = VN_CAST(subargp->dtypep()->skipRefToEnump(), EnumDType); + if (!enumDtp) { + if (const AstVarRef* const varrefp = VN_CAST(subargp, VarRef)) { + enumDtp = VN_CAST(varrefp->varp()->dtypep()->skipRefToEnump(), EnumDType); + } + } + return enumDtp; + } + //---------------------------------------------------------------------- // LOWER LEVEL WIDTH METHODS (none iterate) diff --git a/test_regress/t/t_class_enum.v b/test_regress/t/t_class_enum.v index bd30cd2f8..98c09b246 100644 --- a/test_regress/t/t_class_enum.v +++ b/test_regress/t/t_class_enum.v @@ -4,6 +4,11 @@ // SPDX-FileCopyrightText: 2020 Wilson Snyder // SPDX-License-Identifier: CC0-1.0 +// verilog_format: off +`define stop $stop +`define checks(gotv, expv) do if ((gotv) != (expv)) begin $write("%%Error: %s:%0d: got='%s' exp='%s'\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +// verilog_format: on + module t; class Cls; @@ -12,11 +17,32 @@ module t; B = 20, C = 30 } en_t; + en_t en; + endclass + + class WideCls; + typedef enum logic [95:0] { + A = 96'h1, + B = 96'h2 + } en_t; + en_t en; endclass initial begin Cls c; + WideCls w; + string s; if (c.A != 10) $stop; + c = new; + c.en = c.B; + if (c.en != 20) $stop; + s = $sformatf("%p", c); + `checks(s, "'{en:'h14}"); + + w = new; + w.en = w.B; + s = $sformatf("%p", w); + `checks(s, "'{en:'h2}"); $write("*-* All Finished *-*\n"); $finish; end diff --git a/test_regress/t/t_display_enum_format.py b/test_regress/t/t_display_enum_format.py new file mode 100755 index 000000000..8a938befd --- /dev/null +++ b/test_regress/t/t_display_enum_format.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile() + +test.execute() + +test.passes() diff --git a/test_regress/t/t_display_enum_format.v b/test_regress/t/t_display_enum_format.v new file mode 100644 index 000000000..8eb60f76f --- /dev/null +++ b/test_regress/t/t_display_enum_format.v @@ -0,0 +1,167 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// 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 string empty_no_opt +); + typedef enum logic [1:0] { + E0 = 0, + E1 = 1, + E2 = 2 + } my_e; + + typedef enum logic [63:0] { + W64A = 64'h1, + W64B = 64'h0000_0001_0000_0001 + } wide64_e; + + my_e e; + wide64_e e64; + logic [63:0] n64; +`define check(got, exp) do if ((got) != (exp)) begin \ + $write("%%Error: %s:%0d: got='%s' exp='%s'\n", `__FILE__, `__LINE__, got, exp); \ + $stop; \ + end while(0) + + initial begin + string fmt; + begin + my_e it; + string names_p; + string names_s; + string vals_d; + names_p = ""; + names_s = ""; + vals_d = ""; + for (it = it.first; ; it = it.next) begin + if (names_p != "") begin + names_p = {names_p, ","}; + names_s = {names_s, ","}; + vals_d = {vals_d, ","}; + end + names_p = {names_p, $sformatf("%p", it)}; + names_s = {names_s, $sformatf("%s", it)}; + vals_d = {vals_d, $sformatf("%0d", it)}; + if (it == it.last) break; + end + `check(names_p, "E0,E1,E2"); + `check(names_s, "E0,E1,E2"); + `check(vals_d, "0,1,2"); + end + + // Valid enum values print mnemonic for %p/%s. + e = E0; + `check($sformatf("%p", e), "E0"); + `check($sformatf("%s", e), "E0"); + + e = E1; + `check($sformatf("%p", e), "E1"); + `check($sformatf("%P", e), "E1"); + `check($sformatf("%0p", e), "E1"); + `check($sformatf("%s", e), "E1"); + `check($sformatf("%S", e), "E1"); + `check($sformatf("%d", e), "1"); + `check($sformatf("%0d", e), "1"); + `check($sformatf("%h", e), "1"); + `check($sformatf("%0h", e), "1"); + `check($sformatf("%b", e), "01"); + `check($sformatf("%0b", e), "1"); + `check($sformatf("%o", e), "1"); + `check($sformatf("%0o", e), "1"); + `check($sformatf("%x", e), "1"); + `check($sformatf("%0x", e), "1"); + + e = E2; + `check($sformatf("%p", e), "E2"); + `check($sformatf("%s", e), "E2"); + `check($sformatf("%s|%p", e, e), "E2|E2"); + `check($sformatf("%4p", e), "E2"); + `check($sformatf("%-4p", e), "E2"); + `check($sformatf("%d", e), "2"); + `check($sformatf("%h", e), "2"); + `check($sformatf("%b", e), "10"); + `check($sformatf("%0b", e), "10"); + `check($sformatf("%o", e), "2"); + `check($sformatf("%x", e), "2"); + `check($sformatf("%4d", e), " 2"); + `check($sformatf("%04d", e), "0002"); + `check($sformatf("%4h", e), "0002"); + `check($sformatf("%-4s", e), "E2 "); + `check($sformatf("%4s", e), " E2"); + // `%p`/`%s` in non-terminal positions with mixed formatters. + `check($sformatf("%0d:%s:%0d", 9, e, 7), "9:E2:7"); + `check($sformatf("%s %h %p", e, 4'hA, e), "E2 a E2"); + `check($sformatf("pre %% %s post", e), "pre % E2 post"); + // Complex enum expressions (non-var-ref) in format args. + `check($sformatf("%s", (1'b1 ? E2 : E0)), "E2"); + // 64-bit enums should preserve bits above 32 in both named and numeric cases. + e64 = W64B; + `check($sformatf("%p", e64), "W64B"); + `check($sformatf("%s", e64), "W64B"); + e64 = wide64_e'(64'h0000_0002_0000_0001); + `check($sformatf("%p", e64), "8589934593"); + `check($sformatf("%s", e64), "8589934593"); + n64 = 64'h0000_0000_0000_0001; + `check($sformatf("%0p", n64), "'h1"); + // Exercise display/write-family formatting path in addition to $sformatf checks. + $display("display-valid:%s:%0d:%p", e, 7, e); + $write("write-valid:%s:%0d:%p\n", e, 8, e); + // Invalid enum values fall back to numeric formatting for %p/%s. + e = my_e'(3); + `check($sformatf("%p", e), "3"); + `check($sformatf("%P", e), "3"); + `check($sformatf("%0p", e), "'h3"); + `check($sformatf("%s", e), "3"); + `check($sformatf("%S", e), "3"); + `check($sformatf("%4p", e), "3"); + `check($sformatf("%4s", e), " 3"); + `check($sformatf("%d", e), "3"); + `check($sformatf("%0d", e), "3"); + `check($sformatf("%h", e), "3"); + `check($sformatf("%0h", e), "3"); + `check($sformatf("%b", e), "11"); + `check($sformatf("%0b", e), "11"); + `check($sformatf("%o", e), "3"); + `check($sformatf("%x", e), "3"); + // Non-terminal invalid-value fallback with mixed formatters. + `check($sformatf("%0d:%p:%0d", 9, e, 7), "9:3:7"); + `check($sformatf("%s %h %p", e, 4'hA, e), "3 a 3"); + `check($sformatf("pre %% %s post", e), "pre % 3 post"); + `check($sformatf("%s|%p", e, e), "3|3"); + `check($sformatf("%s", (1'b1 ? my_e'(3) : E0)), "3"); + `check($sformatf("%p", (1'b0 ? E0 : my_e'(3))), "3"); + $display("display-invalid:%s:%0d:%p", e, 7, e); + $write("write-invalid:%s:%0d:%p\n", e, 8, e); + // Runtime-computed $sformatf formats should preserve enum mnemonic/fallback behavior. + e = E2; + fmt = {"%", "s", empty_no_opt}; + `check($sformatf(fmt, e), "E2"); + fmt = {"%", "p", empty_no_opt}; + `check($sformatf(fmt, e), "E2"); + fmt = {"%0d:%", "s", ":%0d", empty_no_opt}; + `check($sformatf(fmt, 9, e, 7), "9:E2:7"); + fmt = {"%", "s", " %h %", "p", empty_no_opt}; + `check($sformatf(fmt, e, 4'hA, e), "E2 a E2"); + e = my_e'(3); + fmt = {"%", "s", empty_no_opt}; + `check($sformatf(fmt, e), "3"); + fmt = {"%", "p", empty_no_opt}; + `check($sformatf(fmt, e), "3"); + fmt = {"%0", "p", empty_no_opt}; + `check($sformatf(fmt, e), "'h3"); + fmt = {"%0d:%", "s", ":%0d", empty_no_opt}; + `check($sformatf(fmt, 9, e, 7), "9:3:7"); + fmt = {"%", "s", " %h %", "p", empty_no_opt}; + `check($sformatf(fmt, e, 4'hA, e), "3 a 3"); + fmt = {"%", "p", empty_no_opt}; + `check($sformatf(fmt, e64), "8589934593"); + + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_enum_huge_methods.v b/test_regress/t/t_enum_huge_methods.v index 06e64eef4..0d9afe845 100644 --- a/test_regress/t/t_enum_huge_methods.v +++ b/test_regress/t/t_enum_huge_methods.v @@ -22,7 +22,7 @@ module t ( integer cyc = 0; my_t e; - string all; + string s; int i_cast; // Check runtime @@ -45,6 +45,10 @@ module t ( `checkh(e.prev, E01); `checkh(e.next(0), ELARGE); `checkh(e.prev(0), ELARGE); + s = $sformatf("%p", e); + `checks(s, "ELARGE"); + s = $sformatf("%s", e); // Non-standard but majority + `checks(s, "ELARGE"); e <= E01; end // @@ -68,6 +72,8 @@ module t ( end else if (cyc == 21) begin `checks(e.name, ""); // Unknown + s = $sformatf("%p", e); + `checks(s, "17"); end else if (cyc == 99) begin $write("*-* All Finished *-*\n"); diff --git a/test_regress/t/t_enum_large_methods.v b/test_regress/t/t_enum_large_methods.v index c8fbb2a16..ca0882ff7 100644 --- a/test_regress/t/t_enum_large_methods.v +++ b/test_regress/t/t_enum_large_methods.v @@ -22,7 +22,7 @@ module t ( integer cyc = 0; my_t e; - string all; + string s; // Check runtime always @(posedge clk) begin @@ -40,6 +40,8 @@ module t ( `checks(e.name, "ELARGE"); `checkh(e.next, E01); `checkh(e.prev, E01); + s = $sformatf("%p", e); + `checks(s, "ELARGE"); e <= E01; end else if (cyc == 20) begin @@ -47,6 +49,8 @@ module t ( end else if (cyc == 21) begin `checks(e.name, ""); // Unknown + s = $sformatf("%p", e); + `checks(s, "17"); end else if (cyc == 99) begin $write("*-* All Finished *-*\n"); diff --git a/test_regress/t/t_enum_type_methods.v b/test_regress/t/t_enum_type_methods.v index 210c668a5..fd295b1ce 100644 --- a/test_regress/t/t_enum_type_methods.v +++ b/test_regress/t/t_enum_type_methods.v @@ -11,8 +11,8 @@ // verilog_format: on module t ( - input clk - ); + input clk +); typedef enum [3:0] { E01 = 1, @@ -20,12 +20,18 @@ module t ( E04 = 4 } my_t; - integer cyc = 0; + integer cyc = 0; my_t e; - int arrayfits [e.num]; // Check can use as constant + int arrayfits[e.num]; // Check can use as constant - string all; + typedef struct { + my_t m_a; + my_t m_b; + } mystr_t; + mystr_t mystr; + + string s; // Check constification initial begin @@ -48,25 +54,38 @@ module t ( `checkh(e.num, 3); `checks(e.name, "E03"); // - all = ""; + s = ""; for (my_t e = e.first; e != e.last; e = e.next) begin - all = {all, e.name}; + s = {s, e.name}; end e = e.last; - all = {all, e.name}; - `checks(all, "E01E03E04"); + s = {s, e.name}; + `checks(s, "E01E03E04"); + // + e = E04; + s = $sformatf("%p", e); + `checks(s, "E04"); + s = $sformatf("%p", E03); + `checks(s, "E03"); + s = $sformatf("%s", E03); // Non-standard but majority + `checks(s, "E03"); + // + mystr.m_a = E03; + mystr.m_b = E04; + s = $sformatf("%p", mystr); + `checks(s, "'{m_a:'h3, m_b:'h4}"); end localparam THREE = 3; // Check runtime - always @ (posedge clk) begin + always @(posedge clk) begin cyc <= cyc + 1; - if (cyc==0) begin + if (cyc == 0) begin // Setup e <= E01; end - else if (cyc==1) begin + else if (cyc == 1) begin `checks(e.name, "E01"); `checkh(e.next, E03); `checkh(e.next(1), E03); @@ -76,7 +95,7 @@ module t ( `checkh(e.prev(2), E03); e <= E03; end - else if (cyc==2) begin + else if (cyc == 2) begin `checks(e.name, "E03"); `checkh(e.next, E04); `checkh(e.next(1), E04); @@ -86,7 +105,7 @@ module t ( `checkh(e.prev(2), E04); e <= E04; end - else if (cyc==3) begin + else if (cyc == 3) begin `checks(e.name, "E04"); `checkh(e.next, E01); `checkh(e.next(1), E01); @@ -95,8 +114,11 @@ module t ( `checkh(e.prev(1), E03); `checkh(e.prev(2), E01); e <= E01; + // + s = $sformatf("%p", e); + `checks(s, "E04"); end - else if (cyc==99) begin + else if (cyc == 99) begin $write("*-* All Finished *-*\n"); $finish; end From 7f01806e154f0b54564b03e6079f29ae7fddfcd2 Mon Sep 17 00:00:00 2001 From: Yogish Sekhar <160029258+ysekhar@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:22:34 +0100 Subject: [PATCH 022/350] Improve FSM coverage detection (#7490) --- src/V3FsmDetect.cpp | 787 ++++++++++++++--- test_regress/t/t_cover_fsm_basic.py | 10 - test_regress/t/t_cover_fsm_basic_summary.out | 7 - test_regress/t/t_cover_fsm_beginif.out | 19 +- test_regress/t/t_cover_fsm_beginif.v | 19 +- .../t/t_cover_fsm_case_next_ok_multi.out | 122 +++ .../t/t_cover_fsm_case_next_ok_multi.py | 41 + .../t/t_cover_fsm_case_next_ok_multi.v | 106 +++ .../t/t_cover_fsm_case_next_ok_multi_incl.out | 122 +++ .../t/t_cover_fsm_combo_same_warn_bad.out | 9 + .../t/t_cover_fsm_combo_same_warn_bad.py | 21 + .../t/t_cover_fsm_combo_same_warn_bad.v | 38 + test_regress/t/t_cover_fsm_decldump.py | 4 +- test_regress/t/t_cover_fsm_enum_bad.out | 6 - test_regress/t/t_cover_fsm_enum_bad.v | 36 - test_regress/t/t_cover_fsm_forced.out | 52 -- test_regress/t/t_cover_fsm_forced.v | 42 - test_regress/t/t_cover_fsm_graphdump.py | 2 +- .../t_cover_fsm_if_unknown_enum_multi_bad.out | 15 + ... t_cover_fsm_if_unknown_enum_multi_bad.py} | 5 +- .../t/t_cover_fsm_if_unknown_enum_multi_bad.v | 140 +++ .../t_cover_fsm_nextstate_overwrite_warn.out | 6 + .../t/t_cover_fsm_nextstate_overwrite_warn.py | 21 + .../t/t_cover_fsm_nextstate_overwrite_warn.v | 32 + .../t_cover_fsm_plain_always_ignore_multi.out | 1 + .../t_cover_fsm_plain_always_ignore_multi.py | 20 + .../t/t_cover_fsm_plain_always_ignore_multi.v | 99 +++ ..._cover_fsm_plain_always_warn_multi_bad.out | 12 + ...t_cover_fsm_plain_always_warn_multi_bad.py | 18 + .../t_cover_fsm_plain_always_warn_multi_bad.v | 106 +++ ...t_cover_fsm_plain_always_zerohit_multi.out | 9 + .../t_cover_fsm_plain_always_zerohit_multi.py | 20 + .../t_cover_fsm_plain_always_zerohit_multi.v | 98 +++ .../t/t_cover_fsm_policy_accept_multi.out | 141 +++ ....py => t_cover_fsm_policy_accept_multi.py} | 4 +- .../t/t_cover_fsm_policy_accept_multi.v | 113 +++ test_regress/t/t_cover_fsm_reset.out | 206 ++++- test_regress/t/t_cover_fsm_reset.py | 26 +- test_regress/t/t_cover_fsm_reset.v | 153 +++- test_regress/t/t_cover_fsm_reset_incl.out | 197 +++++ test_regress/t/t_cover_fsm_reset_multi.out | 69 +- test_regress/t/t_cover_fsm_reset_multi.py | 30 +- test_regress/t/t_cover_fsm_reset_multi.v | 9 +- test_regress/t/t_cover_fsm_reset_summary.out | 7 - test_regress/t/t_cover_fsm_styles.out | 67 -- test_regress/t/t_cover_fsm_styles.v | 55 -- .../t/t_cover_fsm_transition_shapes_multi.out | 1 + ...=> t_cover_fsm_transition_shapes_multi.py} | 11 +- .../t/t_cover_fsm_transition_shapes_multi.v | 819 ++++++++++++++++++ test_regress/t/t_cover_fsm_two_proc_multi.out | 459 ++++++++++ test_regress/t/t_cover_fsm_two_proc_multi.py | 33 + test_regress/t/t_cover_fsm_two_proc_multi.v | 401 +++++++++ test_regress/t/t_cover_otherdecl_dump.py | 2 +- .../t/t_fsmmulti_combo_multi_warn_bad.out | 15 + .../t/t_fsmmulti_combo_multi_warn_bad.py | 16 + .../t/t_fsmmulti_combo_multi_warn_bad.v | 86 ++ test_regress/t/t_fsmmulti_same_bad.out | 3 + test_regress/t/t_fsmmulti_warn_bad.out | 3 + test_regress/t/t_vlcov_fsm_report.out | 2 + test_regress/t/t_vlcov_fsm_report_incl.out | 2 + 60 files changed, 4376 insertions(+), 599 deletions(-) delete mode 100644 test_regress/t/t_cover_fsm_basic_summary.out create mode 100644 test_regress/t/t_cover_fsm_case_next_ok_multi.out create mode 100755 test_regress/t/t_cover_fsm_case_next_ok_multi.py create mode 100644 test_regress/t/t_cover_fsm_case_next_ok_multi.v create mode 100644 test_regress/t/t_cover_fsm_case_next_ok_multi_incl.out create mode 100644 test_regress/t/t_cover_fsm_combo_same_warn_bad.out create mode 100755 test_regress/t/t_cover_fsm_combo_same_warn_bad.py create mode 100644 test_regress/t/t_cover_fsm_combo_same_warn_bad.v delete mode 100644 test_regress/t/t_cover_fsm_enum_bad.out delete mode 100644 test_regress/t/t_cover_fsm_enum_bad.v delete mode 100644 test_regress/t/t_cover_fsm_forced.out delete mode 100644 test_regress/t/t_cover_fsm_forced.v create mode 100644 test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.out rename test_regress/t/{t_cover_fsm_enum_bad.py => t_cover_fsm_if_unknown_enum_multi_bad.py} (63%) create mode 100644 test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.v create mode 100644 test_regress/t/t_cover_fsm_nextstate_overwrite_warn.out create mode 100755 test_regress/t/t_cover_fsm_nextstate_overwrite_warn.py create mode 100644 test_regress/t/t_cover_fsm_nextstate_overwrite_warn.v create mode 100644 test_regress/t/t_cover_fsm_plain_always_ignore_multi.out create mode 100755 test_regress/t/t_cover_fsm_plain_always_ignore_multi.py create mode 100644 test_regress/t/t_cover_fsm_plain_always_ignore_multi.v create mode 100644 test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.out create mode 100755 test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.py create mode 100644 test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.v create mode 100644 test_regress/t/t_cover_fsm_plain_always_zerohit_multi.out create mode 100644 test_regress/t/t_cover_fsm_plain_always_zerohit_multi.py create mode 100644 test_regress/t/t_cover_fsm_plain_always_zerohit_multi.v create mode 100644 test_regress/t/t_cover_fsm_policy_accept_multi.out rename test_regress/t/{t_cover_fsm_forced.py => t_cover_fsm_policy_accept_multi.py} (78%) create mode 100644 test_regress/t/t_cover_fsm_policy_accept_multi.v create mode 100644 test_regress/t/t_cover_fsm_reset_incl.out delete mode 100644 test_regress/t/t_cover_fsm_reset_summary.out delete mode 100644 test_regress/t/t_cover_fsm_styles.out delete mode 100644 test_regress/t/t_cover_fsm_styles.v create mode 100644 test_regress/t/t_cover_fsm_transition_shapes_multi.out rename test_regress/t/{t_cover_fsm_styles.py => t_cover_fsm_transition_shapes_multi.py} (54%) create mode 100644 test_regress/t/t_cover_fsm_transition_shapes_multi.v create mode 100644 test_regress/t/t_cover_fsm_two_proc_multi.out create mode 100755 test_regress/t/t_cover_fsm_two_proc_multi.py create mode 100644 test_regress/t/t_cover_fsm_two_proc_multi.v create mode 100644 test_regress/t/t_fsmmulti_combo_multi_warn_bad.out create mode 100755 test_regress/t/t_fsmmulti_combo_multi_warn_bad.py create mode 100644 test_regress/t/t_fsmmulti_combo_multi_warn_bad.v diff --git a/src/V3FsmDetect.cpp b/src/V3FsmDetect.cpp index 3a9d48877..f09f91163 100644 --- a/src/V3FsmDetect.cpp +++ b/src/V3FsmDetect.cpp @@ -34,6 +34,7 @@ #include #include #include +#include VL_DEFINE_DEBUG_FUNCTIONS; @@ -56,6 +57,69 @@ struct FsmResetCondDesc final { AstVarScope* varScopep = nullptr; }; +class FsmResetArcDesc final { + int m_toValue = 0; // Encoded reset target state. + AstNode* m_nodep = nullptr; // Source node for warnings and emitted metadata. + +public: + FsmResetArcDesc() = default; + FsmResetArcDesc(int toValue, AstNode* nodep) + : m_toValue{toValue} + , m_nodep{nodep} {} + + int toValue() const { return m_toValue; } + AstNode* nodep() const { return m_nodep; } +}; + +class FsmRegisterCandidate final { + AstScope* m_scopep = nullptr; // Owning scope for the paired FSM. + AstAlways* m_alwaysp = nullptr; // Register process that commits the state. + AstVarScope* m_stateVscp = nullptr; // Registered FSM state variable. + AstVarScope* m_nextVscp = nullptr; // Next-state variable or same state var for 1-block FSMs. + std::vector m_senses; // Event controls for recreated coverage blocks. + FsmResetCondDesc m_resetCond; // Saved reset predicate, if any. + std::vector m_resetArcs; // Reset target arcs recovered during detect. + bool m_hasResetCond = false; // Whether the FSM had a modeled reset predicate. + bool m_resetInclude = false; // Whether reset arcs count toward summary totals. + bool m_inclCond = false; // Whether conditional/default arcs are kept explicitly. + +public: + AstScope* scopep() const { return m_scopep; } + void scopep(AstScope* scopep) { m_scopep = scopep; } + AstAlways* alwaysp() const { return m_alwaysp; } + void alwaysp(AstAlways* alwaysp) { m_alwaysp = alwaysp; } + AstVarScope* stateVscp() const { return m_stateVscp; } + void stateVscp(AstVarScope* vscp) { m_stateVscp = vscp; } + AstVarScope* nextVscp() const { return m_nextVscp; } + void nextVscp(AstVarScope* vscp) { m_nextVscp = vscp; } + const std::vector& senses() const { return m_senses; } + std::vector& senses() { return m_senses; } + const FsmResetCondDesc& resetCond() const { return m_resetCond; } + FsmResetCondDesc& resetCond() { return m_resetCond; } + const std::vector& resetArcs() const { return m_resetArcs; } + std::vector& resetArcs() { return m_resetArcs; } + bool hasResetCond() const { return m_hasResetCond; } + void hasResetCond(bool flag) { m_hasResetCond = flag; } + bool resetInclude() const { return m_resetInclude; } + void resetInclude(bool flag) { m_resetInclude = flag; } + bool inclCond() const { return m_inclCond; } + void inclCond(bool flag) { m_inclCond = flag; } +}; + +class FsmComboAlways final { + AstScope* const m_scopep = nullptr; // Owning scope for the combinational process. + AstAlways* const m_alwaysp = nullptr; // Candidate transition process. + +public: + FsmComboAlways() = default; + FsmComboAlways(AstScope* scopep, AstAlways* alwaysp) + : m_scopep{scopep} + , m_alwaysp{alwaysp} {} + + AstScope* scopep() const { return m_scopep; } + AstAlways* alwaysp() const { return m_alwaysp; } +}; + class FsmGraph; class FsmVertex VL_NOT_FINAL : public V3GraphVertex { @@ -152,7 +216,7 @@ public: // context needed to lower states/arcs back into the AST after detection. class FsmGraph final : public V3Graph { AstScope* m_scopep = nullptr; // Owning scoped block for the detected FSM. - AstAlways* m_alwaysp = nullptr; // Original always block being instrumented. + AstAlways* m_stateAlwaysp = nullptr; // Register always block being instrumented. string m_stateVarName; // Pretty state variable name for user-visible output. string m_stateVarInternalName; // Internal state symbol name for dump tags. AstVarScope* m_stateVarScopep = nullptr; // Scoped state variable being tracked. @@ -173,8 +237,8 @@ public: AstScope* scopep() const { return m_scopep; } void scopep(AstScope* scopep) { m_scopep = scopep; } - AstAlways* alwaysp() const { return m_alwaysp; } - void alwaysp(AstAlways* alwaysp) { m_alwaysp = alwaysp; } + AstAlways* stateAlwaysp() const { return m_stateAlwaysp; } + void stateAlwaysp(AstAlways* alwaysp) { m_stateAlwaysp = alwaysp; } const string& stateVarName() const { return m_stateVarName; } void stateVarName(const string& name) { m_stateVarName = name; } const string& stateVarInternalName() const { return m_stateVarInternalName; } @@ -232,13 +296,18 @@ public: struct DetectedFsm final { std::unique_ptr graphp; // Extracted graph for one detected FSM candidate. }; -using DetectedFsmMap = std::map; +using DetectedFsmMap = std::map; + +struct FsmCaseCandidate final { + AstNode* warnNodep = nullptr; // Transition node that made the candidate supported. + AstVarScope* stateVscp = nullptr; // FSM state variable associated with that candidate. +}; // Local shared state between the two adjacent FSM coverage phases. Detection // fills this with recovered FSM graphs; lowering consumes the completed graphs // immediately afterward without needing any AST serialization bridge. class FsmState final { - // All detected FSMs keyed by state varscope name. This is the only bridge + // All detected FSMs keyed by state varscope identity. This is the only bridge // between the adjacent detect and lower phases, so the second phase never // needs to rediscover or serialize the extracted machine. DetectedFsmMap m_fsms; @@ -258,6 +327,10 @@ class FsmDetectVisitor final : public VNVisitor { // STATE - for current visit position (use VL_RESTORER) FsmState& m_state; AstScope* m_scopep = nullptr; + std::unordered_map m_registerCandidates; + std::vector m_comboAlwayss; + std::vector m_nonComboAlwayss; + std::unordered_map m_comboPaired; // METHODS // Enum-backed FSMs may be wrapped in refs/typedefs; normalize to the @@ -266,6 +339,142 @@ class FsmDetectVisitor final : public VNVisitor { return dtypep->skipRefToEnump(); } + static string candidateConflictContext(AstNode* laterNodep, + const FsmCaseCandidate& firstCand) { + return '\n' + laterNodep->warnContextPrimary() + firstCand.warnNodep->warnOther() + + "... Location of first supported candidate for " + + firstCand.stateVscp->prettyNameQ() + '\n' + + firstCand.warnNodep->warnContextSecondary(); + } + + class RegisterAlwaysAnalyzer final { + AstScope* const m_scopep; + + public: + explicit RegisterAlwaysAnalyzer(AstScope* scopep) + : m_scopep{scopep} {} + + std::vector> oneBlockCandidates(AstAlways* alwaysp) const { + std::vector> candidates; + AstNode* const stmtsp = alwaysp->stmtsp(); + AstIf* const firstIfp = VN_CAST(stmtsp, If); + if (firstIfp) { + if (AstCase* const casep = VN_CAST(firstIfp->elsesp(), Case)) { + candidates.emplace_back( + casep, FsmDetectVisitor::isSimpleResetCond(firstIfp->condp()) + ? firstIfp->condp() + : nullptr); + } + } + for (AstNode* nodep = stmtsp; nodep; nodep = nodep->nextp()) { + if (AstCase* const casep = VN_CAST(nodep, Case)) + candidates.emplace_back(casep, nullptr); + } + return candidates; + } + + bool matchRegisterCandidate(AstAlways* alwaysp, FsmRegisterCandidate& cand) const { + return FsmDetectVisitor::matchRegisterAlways(alwaysp, m_scopep, cand); + } + + void buildOneBlockCandidate(AstAlways* alwaysp, AstVarScope* vscp, + AstNodeExpr* resetCondp, FsmRegisterCandidate& reg) const { + reg.scopep(m_scopep); + reg.alwaysp(alwaysp); + reg.stateVscp(vscp); + reg.nextVscp(vscp); + reg.senses() = FsmDetectVisitor::describeSenTree(alwaysp->sentreep()); + reg.resetCond() = FsmDetectVisitor::describeResetCond(resetCondp); + reg.hasResetCond(reg.resetCond().varScopep != nullptr); + reg.resetInclude(vscp->varp()->attrFsmResetArc()); + reg.inclCond(vscp->varp()->attrFsmArcInclCond()); + AstIf* const firstIfp = VN_CAST(alwaysp->stmtsp(), If); + if (firstIfp && reg.hasResetCond()) { + AstVarScope* resetStateVscp = nullptr; + const ResetAssignStatus resetStatus = FsmDetectVisitor::collectConstStateAssigns( + firstIfp->thensp(), resetStateVscp, reg.resetArcs()); + if (resetStatus == ResetAssignStatus::NONE || resetStateVscp != vscp) { + reg.resetArcs().clear(); + int resetValue = 0; + AstNode* const thenNodep + = FsmDetectVisitor::singleMeaningfulBranch(firstIfp->thensp()); + UASSERT_OBJ(thenNodep, firstIfp, + "one-block reset fallback requires a non-empty reset branch"); + if (FsmDetectVisitor::directConstStateAssignNode(thenNodep, resetStateVscp, + resetValue) + && resetStateVscp == vscp) { + reg.resetArcs().emplace_back(resetValue, firstIfp->thensp()); + } + } else if (resetStatus == ResetAssignStatus::MULTI_SAME_STATE) { + reg.resetArcs().clear(); + } + } + } + }; + + class ComboAlwaysAnalyzer final { + public: + struct ComboMatch final { + const FsmRegisterCandidate* matchedp = nullptr; + AstNode* warnNodep = nullptr; + }; + + private: + const std::unordered_map& m_registerCandidates; + + public: + explicit ComboAlwaysAnalyzer( + const std::unordered_map& registerCandidates) + : m_registerCandidates{registerCandidates} {} + + ComboMatch matchCase(AstNode* stmtsp, AstCase* casep) const { + ComboMatch match; + AstVarRef* const selp = VN_CAST(casep->exprp(), VarRef); + if (!selp) return match; + for (const auto& it : m_registerCandidates) { + const FsmRegisterCandidate& reg = it.second; + if (selp->varScopep() == reg.nextVscp()) { + if (!FsmDetectVisitor::hasCanonicalNextStateDefaultBeforeCase( + stmtsp, casep, reg.stateVscp(), reg.nextVscp())) { + continue; + } + } else if (selp->varScopep() != reg.stateVscp()) { + continue; + } + AstNode* const warnNodep + = FsmDetectVisitor::caseSupportedTransitionNode(casep, reg.nextVscp(), + reg.inclCond()); + if (!warnNodep) continue; + match.matchedp = ® + match.warnNodep = warnNodep; + } + return match; + } + + bool shouldWarnUnsupported(AstNode* stmtsp, AstCase* casep) const { + const AstVarRef* const selp = VN_CAST(casep->exprp(), VarRef); + if (!selp) return false; + + const auto isRecognizedFsm = [&](const auto& entry) -> bool { + const FsmRegisterCandidate& reg = entry.second; + const bool matchesNext = selp->varScopep() == reg.nextVscp(); + const bool matchesState = selp->varScopep() == reg.stateVscp(); + + if (!matchesNext && !matchesState) return false; + if (matchesNext + && !FsmDetectVisitor::hasCanonicalNextStateDefaultBeforeCase( + stmtsp, casep, reg.stateVscp(), reg.nextVscp())) { + return false; + } + return FsmDetectVisitor::caseSupportedTransitionNode(casep, reg.nextVscp(), + reg.inclCond()); + }; + + return std::any_of(m_registerCandidates.begin(), m_registerCandidates.end(), + isRecognizedFsm); + } + }; + // Reset arcs are only modeled for the simple signal form that survives to // this pass after earlier normalization. static bool isSimpleResetCond(AstNodeExpr* condp) { return VN_IS(condp, VarRef); } @@ -303,10 +512,15 @@ class FsmDetectVisitor final : public VNVisitor { // rather than other instrumentation already attached to the block. static bool isIgnorableStmt(AstNode* nodep) { return VN_IS(nodep, CoverInc); } - // Conservative extractor: only treat a branch as simple when exactly one - // non-coverage statement remains after unwrapping. Richer multi-statement - // or control-flow forms are intentionally left for follow-on FSM-detection - // work instead of being partially inferred here. + static AstNode* skipLeadingIgnorableStmt(AstNode* nodep) { + while (nodep && isIgnorableStmt(nodep)) nodep = nodep->nextp(); + return nodep; + } + + // Conservative extractor for statement lists: only treat a list as simple + // when exactly one non-coverage statement remains after unwrapping. + // Richer multi-statement or control-flow forms are intentionally left for + // follow-on FSM-detection work instead of being partially inferred here. static AstNode* singleMeaningfulStmt(AstNode* stmtp) { AstNode* resultp = nullptr; for (AstNode* nodep = stmtp; nodep; nodep = nodep->nextp()) { @@ -317,12 +531,49 @@ class FsmDetectVisitor final : public VNVisitor { return resultp; } - // Recognize the direct "state <= X" form that gives us an unambiguous arc - // target without needing deeper control-flow reasoning. Branches that fall - // out here represent currently unsupported next-state shapes rather than - // bugs in the implemented subset. + // If/else branches are a single subtree, not a statement list, so do not + // walk nextp() here or we may accidentally consume the sibling else-arm. + static AstNode* singleMeaningfulBranch(AstNode* branchp) { + if (!branchp) return nullptr; + return branchp; + } + + // By fsm-detect time, non-clocked always @* blocks are already admitted through + // a missing sentree. This helper therefore only needs to recognize + // explicit changed-sensitivity lists such as always @(a or b); clocked and + // event-driven forms remain out of scope. + static bool isPlainComboSentree(const AstSenTree* sentreep) { + UASSERT(sentreep, "plain combo sensitivity check requires a sensitivity tree"); + for (const AstSenItem* senp = sentreep->sensesp(); senp; + senp = VN_AS(senp->nextp(), SenItem)) { + if (senp->edgeType() == VEdgeType::ET_CHANGED) continue; + return false; + } + return true; + } + + void warnUnsupportedComboAlways(const FsmComboAlways& combo) { + const ComboAlwaysAnalyzer analyzer{m_registerCandidates}; + AstNode* const stmtsp = skipLeadingIgnorableStmt(combo.alwaysp()->stmtsp()); + bool warned = false; + for (AstNode* nodep = stmtsp; nodep; nodep = nodep->nextp()) { + AstCase* const casep = VN_CAST(nodep, Case); + if (!casep) continue; + if (analyzer.shouldWarnUnsupported(stmtsp, casep)) { + casep->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on non-clocked always " + "blocks requires a combinational sensitivity list or " + "always_comb"); + warned = true; + } + if (warned) break; + } + } + + // Case-item bodies are single subtrees like if/else arms, not statement + // lists, so unwrap only local begin/end wrappers here rather than walking + // sibling case items via nextp(). static AstNodeAssign* directStateAssign(AstNode* stmtp, AstVarScope* stateVscp) { - AstNode* const nodep = singleMeaningfulStmt(stmtp); + AstNode* const nodep = singleMeaningfulBranch(stmtp); if (!nodep) return nullptr; AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign); if (!assp) return nullptr; @@ -331,6 +582,177 @@ class FsmDetectVisitor final : public VNVisitor { return assp; } + static AstNodeAssign* nodeStateVarAssign(AstNode* nodep, AstVarScope*& stateVscp, + AstVarScope*& fromVscp) { + AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign); + if (!assp) return nullptr; + AstVarRef* const lhsp = VN_AS(assp->lhsp(), VarRef); + UASSERT_OBJ(lhsp, assp, "register commit lhs should be normalized to a VarRef"); + AstVarRef* const rhsp = VN_CAST(assp->rhsp(), VarRef); + if (!rhsp) return nullptr; + stateVscp = lhsp->varScopep(); + fromVscp = rhsp->varScopep(); + return assp; + } + + static AstNodeAssign* directCondStateVarAssign(AstNode* nodep, AstVarScope*& stateVscp, + AstVarScope*& fromVscp, AstNodeExpr*& condp, + int& resetValue) { + AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign); + if (!assp) return nullptr; + AstVarRef* const lhsp = VN_AS(assp->lhsp(), VarRef); + UASSERT_OBJ(lhsp, assp, + "conditional register commit lhs should be normalized to a VarRef"); + AstCond* const rhsp = VN_CAST(assp->rhsp(), Cond); + if (!rhsp) return nullptr; + AstVarRef* const elsep = VN_CAST(rhsp->elsep(), VarRef); + if (!elsep || !exprConstValue(rhsp->thenp(), resetValue)) return nullptr; + stateVscp = lhsp->varScopep(); + fromVscp = elsep->varScopep(); + condp = rhsp->condp(); + return assp; + } + + static AstNodeAssign* directConstStateAssignNode(AstNode* nodep, AstVarScope*& stateVscp, + int& value) { + AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign); + if (!assp) return nullptr; + AstVarRef* const lhsp = VN_AS(assp->lhsp(), VarRef); + UASSERT_OBJ(lhsp, assp, + "direct constant state assignment lhs should be normalized to a VarRef"); + if (!exprConstValue(assp->rhsp(), value)) return nullptr; + stateVscp = lhsp->varScopep(); + return assp; + } + + enum class ResetAssignStatus : uint8_t { + NONE, // Reset branch was not the supported direct-constant shape. + SINGLE, // Exactly one supported reset assignment was collected. + MULTI_SAME_STATE // Multiple assignments to the same FSM state var; warn and ignore. + }; + + // Reset arcs are only extracted from the single direct-constant form. If + // user RTL assigns the same state register multiple times in the reset + // branch, warn and skip reset-arc modeling rather than inventing multiple + // reset transitions for an odd but legal coding style. + static ResetAssignStatus collectConstStateAssigns(AstNode* stmtp, AstVarScope*& stateVscp, + std::vector& resetArcs) { + AstNode* nodep = skipLeadingIgnorableStmt(stmtp); + UASSERT_OBJ(nodep, stmtp, "Empty reset branch unexpectedly survived to FSM detection"); + for (;; nodep = nodep->nextp()) { + AstVarScope* assignStateVscp = nullptr; + int value = 0; + AstNodeAssign* const assp = directConstStateAssignNode(nodep, assignStateVscp, value); + if (!assp) return ResetAssignStatus::NONE; + if (!stateVscp) stateVscp = assignStateVscp; + if (assignStateVscp != stateVscp) return ResetAssignStatus::NONE; + if (!resetArcs.empty()) { + assp->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on reset branches with " + "multiple assignments to the state variable"); + resetArcs.clear(); + return ResetAssignStatus::MULTI_SAME_STATE; + } + resetArcs.emplace_back(value, assp); + if (!nodep->nextp()) return ResetAssignStatus::SINGLE; + } + } + + static bool hasCanonicalNextStateDefaultBeforeCase(AstNode* stmtsp, AstCase* casep, + AstVarScope* stateVscp, + AstVarScope* nextVscp) { + AstNode* const bodyp = skipLeadingIgnorableStmt(stmtsp); + bool sawCanonicalDefault = false; + for (AstNode* nodep = bodyp;; nodep = nodep->nextp()) { + UASSERT_OBJ(nodep, casep, + "case(state_d) candidate not found in scanned statement list"); + if (nodep == casep) return sawCanonicalDefault; + if (AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign)) { + AstVarRef* const lhsp = VN_CAST(assp->lhsp(), VarRef); + AstVarRef* const rhsp = VN_CAST(assp->rhsp(), VarRef); + if (!lhsp || lhsp->varScopep() != nextVscp) continue; + if (sawCanonicalDefault) { + const string nextName = nextVscp->varp()->prettyNameQ(); + const string stateName = stateVscp->varp()->prettyNameQ(); + assp->v3warn(COVERIGN, + "Ignoring unsupported: FSM coverage on case(" + nextName + + ") when the canonical " + nextName + " = " + stateName + + " default is overwritten before the case statement"); + return false; + } + if (!rhsp || rhsp->varScopep() != stateVscp) return false; + sawCanonicalDefault = true; + } + } + } + + static bool ifStateConstAssign(AstNode* stmtp, AstVarScope* stateVscp, int& thenValue, + int& elseValue) { + AstIf* const ifp = VN_CAST(singleMeaningfulBranch(stmtp), If); + if (!ifp || !ifp->elsesp()) return false; + AstVarScope* thenVscp = nullptr; + AstVarScope* elseVscp = nullptr; + AstNode* const thenNodep = singleMeaningfulBranch(skipLeadingIgnorableStmt(ifp->thensp())); + UASSERT_OBJ(thenNodep, ifp, "Empty then-branch unexpectedly survived to FSM detection"); + AstNode* const elseNodep = singleMeaningfulBranch(skipLeadingIgnorableStmt(ifp->elsesp())); + if (!elseNodep) return false; + if (!directConstStateAssignNode(thenNodep, thenVscp, thenValue)) return false; + if (!directConstStateAssignNode(elseNodep, elseVscp, elseValue)) return false; + if (thenVscp == stateVscp && elseVscp == stateVscp) return true; + if (thenVscp != elseVscp) return false; + AstNode* const followp = skipLeadingIgnorableStmt(ifp->nextp()); + AstVarScope* finalStateVscp = nullptr; + AstVarScope* finalFromVscp = nullptr; + AstNode* const finalNodep = singleMeaningfulBranch(followp); + if (!finalNodep) return false; + if (!nodeStateVarAssign(finalNodep, finalStateVscp, finalFromVscp)) return false; + if (finalStateVscp != stateVscp) return false; + if (finalFromVscp != thenVscp) return false; + return true; + } + + static bool directStateCondConstAssign(AstNode* stmtp, AstVarScope* stateVscp, int& thenValue, + int& elseValue) { + AstNodeAssign* const assp = directStateAssign(stmtp, stateVscp); + if (!assp) return false; + AstCond* const condp = VN_CAST(assp->rhsp(), Cond); + if (!condp) return false; + return exprConstValue(condp->thenp(), thenValue) + && exprConstValue(condp->elsep(), elseValue); + } + + static AstNode* caseItemSupportedArcNode(AstCaseItem* itemp, AstVarScope* stateVscp, + bool inclCond) { + if (itemp->isDefault()) { + if (!inclCond) return nullptr; + } + AstNodeAssign* const assp = directStateAssign(itemp->stmtsp(), stateVscp); + if (assp) { + int toValue = 0; + if (exprConstValue(assp->rhsp(), toValue)) return assp; + } + int thenValue = 0; + int elseValue = 0; + if (directStateCondConstAssign(itemp->stmtsp(), stateVscp, thenValue, elseValue)) { + return assp; + } + if (ifStateConstAssign(itemp->stmtsp(), stateVscp, thenValue, elseValue)) { + return singleMeaningfulBranch(itemp->stmtsp()); + } + return nullptr; + } + + // Combinational transition blocks are paired only through supported case + // items that assign to the recorded next-state variable. + static AstNode* caseSupportedTransitionNode(AstCase* casep, AstVarScope* stateVscp, + bool inclCond) { + for (AstCaseItem* itemp = casep->itemsp(); itemp; + itemp = VN_AS(itemp->nextp(), CaseItem)) { + if (AstNode* const nodep = caseItemSupportedArcNode(itemp, stateVscp, inclCond)) + return nodep; + } + return nullptr; + } + // Prefer enum labels in reports; fall back to synthetic labels for forced // non-enum FSMs so coverage points remain human-readable. static string labelForValue(const std::unordered_map& labels, int value) { @@ -356,10 +778,105 @@ class FsmDetectVisitor final : public VNVisitor { const std::unordered_map& labels, int value) { if (labels.find(value) != labels.end()) return true; nodep->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on enum state transitions " - "that assign a constant not present in the declared enum"); + "that assign a constant that is not present in the declared " + "enum"); return false; } + // Strict Phase 1 matcher for register processes: either a bare state + // commit, or a top-level reset guard whose else path is that commit. + static bool matchRegisterAlways(AstAlways* alwaysp, AstScope* scopep, + FsmRegisterCandidate& cand) { + if (!alwaysp->sentreep() || !alwaysp->sentreep()->hasEdge()) return false; + + AstNode* const stmtsp = skipLeadingIgnorableStmt(alwaysp->stmtsp()); + AstNode* const nodep = singleMeaningfulStmt(stmtsp); + if (!nodep) return false; + + AstVarScope* stateVscp = nullptr; + AstVarScope* nextVscp = nullptr; + if (AstIf* const ifp = VN_CAST(nodep, If)) { + if (!ifp->elsesp() || !isSimpleResetCond(ifp->condp())) return false; + AstVarScope* resetStateVscp = nullptr; + const ResetAssignStatus resetStatus + = collectConstStateAssigns(ifp->thensp(), resetStateVscp, cand.resetArcs()); + if (resetStatus == ResetAssignStatus::NONE) { + cand.resetArcs().clear(); + int resetValue = 0; + AstNode* const thenNodep = singleMeaningfulBranch(ifp->thensp()); + UASSERT_OBJ(thenNodep, ifp, "reset fallback requires a non-empty reset branch"); + if (!directConstStateAssignNode(thenNodep, resetStateVscp, resetValue)) + return false; + cand.resetArcs().emplace_back(resetValue, ifp->thensp()); + } else if (resetStatus == ResetAssignStatus::MULTI_SAME_STATE) { + cand.resetArcs().clear(); + } + AstNode* const elseNodep = singleMeaningfulBranch(ifp->elsesp()); + UASSERT_OBJ(elseNodep, ifp, "register reset match requires a non-empty commit branch"); + if (!nodeStateVarAssign(elseNodep, stateVscp, nextVscp)) return false; + if (resetStateVscp != stateVscp) return false; + cand.resetCond() = describeResetCond(ifp->condp()); + cand.hasResetCond(cand.resetCond().varScopep != nullptr); + } else { + AstNodeExpr* resetCondp = nullptr; + int resetValue = 0; + if (AstNodeAssign* const assp + = directCondStateVarAssign(nodep, stateVscp, nextVscp, resetCondp, resetValue)) { + cand.resetArcs().emplace_back(resetValue, assp); + cand.resetCond() = describeResetCond(resetCondp); + cand.hasResetCond(cand.resetCond().varScopep != nullptr); + } else if (!nodeStateVarAssign(nodep, stateVscp, nextVscp)) { + return false; + } + } + cand.scopep(scopep); + cand.alwaysp(alwaysp); + cand.stateVscp(stateVscp); + cand.nextVscp(nextVscp); + cand.senses() = describeSenTree(alwaysp->sentreep()); + cand.resetInclude(stateVscp->varp()->attrFsmResetArc()); + cand.inclCond(stateVscp->varp()->attrFsmArcInclCond()); + return true; + } + + // Build the Phase 1 state space from the tracked registered state + // variable, not from whichever signal the transition case happened to use. + static bool collectStateLabels(AstNode* nodep, AstVarScope* stateVscp, + std::vector>& states, + std::unordered_map& labels) { + AstVar* const stateVarp = stateVscp->varp(); + AstEnumDType* enump = VN_CAST(unwrapEnumCandidate(stateVscp->dtypep()), EnumDType); + if (!enump) enump = VN_CAST(unwrapEnumCandidate(stateVarp->dtypep()), EnumDType); + const bool forced = stateVarp->attrFsmState(); + if (!enump && !forced) return false; + + if (enump) { + if (stateVscp->width() > 32) { + nodep->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on enum-typed state " + "variables wider than 32 bits"); + return false; + } + for (AstEnumItem* itemp = enump->itemsp(); itemp; + itemp = VN_AS(itemp->nextp(), EnumItem)) { + const AstConst* const constp = VN_AS(itemp->valuep(), Const); + const int value = constp->toSInt(); + states.emplace_back(itemp->name(), value); + labels.emplace(value, itemp->name()); + } + return states.size() >= 2; + } + + const int width = stateVarp->width(); + if (width >= 31) return false; + const unsigned stateCount = 1U << width; + for (unsigned value = 0; value < stateCount; ++value) { + const string label = "S" + cvtToStr(value); + states.emplace_back(label, static_cast(value)); + labels.emplace(static_cast(value), label); + } + return true; + } + // Extract supported case-item transitions in one place so the conservative // policy for direct and ternary forms stays consistent. The false exits in // this helper are deliberate subset boundaries: they document shapes we do @@ -390,24 +907,21 @@ class FsmDetectVisitor final : public VNVisitor { } return true; } + } - if (AstCond* const condp = VN_CAST(assp->rhsp(), Cond)) { - int thenValue = 0; - int elseValue = 0; - const bool simpleCond = exprConstValue(condp->thenp(), thenValue) - && exprConstValue(condp->elsep(), elseValue); - if (simpleCond || inclCond) { - if (!validateKnownStateValue(condp->thenp(), labels, thenValue)) return true; - if (!validateKnownStateValue(condp->elsep(), labels, elseValue)) return true; - for (const int branchValue : {thenValue, elseValue}) { - for (const std::pair& from : froms) { - graph.addArc(from.second, branchValue, false, true, itemp->isDefault(), - assp->fileline()); - } - } - return true; + int thenValue = 0; + int elseValue = 0; + if (directStateCondConstAssign(itemp->stmtsp(), stateVscp, thenValue, elseValue) + || ifStateConstAssign(itemp->stmtsp(), stateVscp, thenValue, elseValue)) { + if (!validateKnownStateValue(itemp->stmtsp(), labels, thenValue)) return true; + if (!validateKnownStateValue(itemp->stmtsp(), labels, elseValue)) return true; + for (const int branchValue : {thenValue, elseValue}) { + for (const std::pair& from : froms) { + graph.addArc(from.second, branchValue, false, true, itemp->isDefault(), + itemp->stmtsp()->fileline()); } } + return true; } return false; @@ -415,82 +929,45 @@ class FsmDetectVisitor final : public VNVisitor { // Reset transitions are described separately because they live in the reset // branch outside the steady-state case statement. - static void addResetArcs(FsmGraph& graph, AstNode* stmtsp, AstVarScope* stateVscp, + static void addResetArcs(FsmGraph& graph, const std::vector& resetArcs, const std::unordered_map& labels) { - for (AstNode* nodep = stmtsp; nodep; nodep = nodep->nextp()) { - if (AstNodeAssign* const assp = VN_CAST(nodep, NodeAssign)) { - AstVarRef* const vrefp = VN_CAST(assp->lhsp(), VarRef); - int toValue = 0; - if (vrefp && vrefp->varScopep() == stateVscp - && exprConstValue(assp->rhsp(), toValue)) { - if (!validateKnownStateValue(assp, labels, toValue)) continue; - graph.addArc(0, toValue, true, false, false, assp->fileline()); - } - } + for (const FsmResetArcDesc& resetArc : resetArcs) { + if (!validateKnownStateValue(resetArc.nodep(), labels, resetArc.toValue())) continue; + graph.addArc(0, resetArc.toValue(), true, false, false, resetArc.nodep()->fileline()); } } // Turn one candidate case statement into the graph representation that the // later lowering phase will consume directly, while reviewers can still // inspect the extracted machine via DOT dumps. - void processCase(AstCase* casep, AstNodeExpr* resetCondp, AstAlways* alwaysp) { - AstVarRef* const selp = VN_CAST(casep->exprp(), VarRef); - if (!selp) return; - AstVarScope* const stateVscp = selp->varScopep(); - AstVar* const stateVarp = selp->varp(); - AstEnumDType* enump = VN_CAST(unwrapEnumCandidate(stateVscp->dtypep()), EnumDType); - if (!enump) enump = VN_CAST(unwrapEnumCandidate(stateVarp->dtypep()), EnumDType); - const bool forced = stateVarp->attrFsmState(); - if (!enump && !forced) return; - + void processCase(AstCase* casep, AstVarScope* assignVscp, const FsmRegisterCandidate& reg) { + UASSERT_OBJ(assignVscp, casep, "FSM case processing requires a non-null assignment var"); + AstVarScope* const stateVscp = reg.stateVscp(); std::vector> states; std::unordered_map labels; - if (enump) { - if (stateVscp->width() < 1 || stateVscp->width() > 32) { - casep->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on enum-typed state " - "variables wider than 32 bits"); - return; - } - for (AstEnumItem* itemp = enump->itemsp(); itemp; - itemp = VN_AS(itemp->nextp(), EnumItem)) { - const AstConst* const constp = VN_AS(itemp->valuep(), Const); - const int value = constp->toSInt(); - states.emplace_back(itemp->name(), value); - labels.emplace(value, itemp->name()); - } - if (states.size() < 2) return; - } else { - const int width = stateVarp->width(); - if (width < 1 || width >= 31) return; - const unsigned stateCount = 1U << width; - for (unsigned value = 0; value < stateCount; ++value) { - const string label = "S" + cvtToStr(value); - states.emplace_back(label, static_cast(value)); - labels.emplace(static_cast(value), label); - } - } - - DetectedFsm& entry = m_state.fsms()[stateVscp->name()]; + if (!collectStateLabels(casep, stateVscp, states, labels)) return; + DetectedFsm& entry = m_state.fsms()[stateVscp]; if (!entry.graphp) { entry.graphp.reset(new FsmGraph{}); - entry.graphp->scopep(m_scopep); - entry.graphp->alwaysp(alwaysp); + entry.graphp->scopep(reg.scopep()); + entry.graphp->stateAlwaysp(reg.alwaysp()); entry.graphp->stateVarName(stateVscp->prettyName()); - entry.graphp->stateVarInternalName(stateVarp->name()); + entry.graphp->stateVarInternalName(stateVscp->varp()->name()); entry.graphp->stateVarScopep(stateVscp); - entry.graphp->senses() = describeSenTree(alwaysp->sentreep()); - entry.graphp->resetCond() = describeResetCond(resetCondp); - entry.graphp->hasResetCond(entry.graphp->resetCond().varScopep != nullptr); - entry.graphp->resetInclude(stateVarp->attrFsmResetArc()); - entry.graphp->inclCond(stateVarp->attrFsmArcInclCond()); + entry.graphp->senses() = reg.senses(); + entry.graphp->resetCond() = reg.resetCond(); + entry.graphp->hasResetCond(reg.hasResetCond()); + entry.graphp->resetInclude(reg.resetInclude()); + entry.graphp->inclCond(reg.inclCond()); entry.graphp->fileline(casep->fileline()); for (const std::pair& state : states) { entry.graphp->addStateVertex(state.first, state.second); } + addResetArcs(*entry.graphp, reg.resetArcs(), labels); } for (AstCaseItem* itemp = casep->itemsp(); itemp; itemp = VN_AS(itemp->nextp(), CaseItem)) { - emitCaseItemArcs(*entry.graphp, itemp, stateVscp, labels, entry.graphp->inclCond()); + emitCaseItemArcs(*entry.graphp, itemp, assignVscp, labels, entry.graphp->inclCond()); } } @@ -499,57 +976,93 @@ class FsmDetectVisitor final : public VNVisitor { // filtering stays narrow on purpose: we prefer to skip ambiguous shapes now // and expand detection in a later PR rather than over-infer coverage from // forms we do not yet model confidently. - void processAlways(AstAlways* alwaysp) { - if (!alwaysp->sentreep() || !alwaysp->sentreep()->hasClocked()) return; - std::vector> candidates; - AstNode* stmtsp = alwaysp->stmtsp(); - AstIf* const firstIfp = VN_CAST(stmtsp, If); - if (firstIfp) { - if (AstCase* const casep = VN_CAST(firstIfp->elsesp(), Case)) { - candidates.emplace_back( - casep, isSimpleResetCond(firstIfp->condp()) ? firstIfp->condp() : nullptr); - } - } - for (AstNode* nodep = stmtsp; nodep; nodep = nodep->nextp()) { - if (AstCase* const casep = VN_CAST(nodep, Case)) - candidates.emplace_back(casep, nullptr); - } + void processOneBlockAlways(AstAlways* alwaysp) { + const RegisterAlwaysAnalyzer analyzer{m_scopep}; + if (!alwaysp->sentreep() || !alwaysp->sentreep()->hasEdge()) return; + const std::vector> candidates + = analyzer.oneBlockCandidates(alwaysp); if (candidates.empty()) return; - AstVarScope* firstVscp = nullptr; + FsmCaseCandidate firstCand; for (const std::pair& cand : candidates) { AstVarRef* const selp = VN_CAST(cand.first->exprp(), VarRef); AstVarScope* const vscp = selp ? selp->varScopep() : nullptr; if (!vscp) continue; - if (!firstVscp) { - firstVscp = vscp; - processCase(cand.first, cand.second, alwaysp); - } else if (vscp != firstVscp) { + if (!firstCand.stateVscp) { + firstCand.warnNodep = cand.first; + firstCand.stateVscp = vscp; + FsmRegisterCandidate reg; + analyzer.buildOneBlockCandidate(alwaysp, vscp, cand.second, reg); + processCase(cand.first, vscp, reg); + } else if (vscp != firstCand.stateVscp) { cand.first->v3warn(FSMMULTI, "FSM coverage: multiple enum-typed case statements found in " "the same always block. Only the first candidate will be " - "instrumented."); + "instrumented." + << candidateConflictContext(cand.first, firstCand)); } else { cand.first->v3warn(COVERIGN, "Ignoring unsupported: FSM coverage on multiple supported case " "statements found in the same always block. Only the first " - "candidate will be instrumented."); + "candidate will be instrumented." + << candidateConflictContext(cand.first, firstCand)); } } + } - if (!(firstIfp && firstVscp)) return; - const DetectedFsmMap& fsms = m_state.fsms(); - const DetectedFsmMap::const_iterator it = fsms.find(firstVscp->name()); - if (it == fsms.end()) return; - FsmGraph* const graphp = it->second.graphp.get(); - if (!graphp->hasResetCond()) return; - std::unordered_map labels; - for (const V3GraphVertex& vtx : graphp->vertices()) { - const FsmVertex* const vertexp = vtx.as(); - if (!vertexp->isState()) continue; - labels.emplace(vertexp->value(), vertexp->label()); + // Phase 1 two-process pairing scans combinational always blocks only after + // all strict register candidates have been collected, so source order does + // not matter. + static void warnComboSameAlways(AstNode* warnNodep, const FsmCaseCandidate& firstCand) { + warnNodep->v3warn(FSMMULTI, + "FSM coverage: multiple supported transition candidates found in " + "the same combinational always block. Only the first candidate " + "will be instrumented." + << candidateConflictContext(warnNodep, firstCand)); + } + + void processComboAlways(const FsmComboAlways& combo) { + const ComboAlwaysAnalyzer analyzer{m_registerCandidates}; + AstNode* const stmtsp = skipLeadingIgnorableStmt(combo.alwaysp()->stmtsp()); + FsmCaseCandidate firstCand; + for (AstNode* nodep = stmtsp; nodep; nodep = nodep->nextp()) { + AstCase* const casep = VN_CAST(nodep, Case); + if (!casep) continue; + const ComboAlwaysAnalyzer::ComboMatch match = analyzer.matchCase(stmtsp, casep); + const FsmRegisterCandidate* const matchedp = match.matchedp; + AstNode* const matchedWarnNodep = match.warnNodep; + if (!matchedp) continue; + if (!firstCand.stateVscp) { + const auto insertPair = m_comboPaired.emplace( + matchedp->stateVscp(), FsmCaseCandidate{matchedWarnNodep, + const_cast( + matchedp->stateVscp())}); + if (!insertPair.second) { + matchedWarnNodep->v3warn( + FSMMULTI, + "FSM coverage: multiple supported transition candidates found " + "for the same FSM in combinational always blocks. Only the " + "first candidate will be instrumented." + << candidateConflictContext(matchedWarnNodep, insertPair.first->second)); + continue; + } + firstCand.warnNodep = matchedWarnNodep; + firstCand.stateVscp = const_cast(matchedp->stateVscp()); + processCase(casep, matchedp->nextVscp(), *matchedp); + continue; + } + if (matchedp->stateVscp() != firstCand.stateVscp) { + warnComboSameAlways(matchedWarnNodep, firstCand); + continue; + } + matchedWarnNodep->v3warn(COVERIGN, + "Ignoring unsupported: FSM coverage on multiple " + "supported case statements found in the same " + "combinational always block. Only the first " + "candidate will be instrumented." + << candidateConflictContext(matchedWarnNodep, + firstCand)); } - addResetArcs(*graphp, firstIfp->thensp(), firstVscp, labels); } // Track the current scope so each detected FSM records the module/scope @@ -560,8 +1073,26 @@ class FsmDetectVisitor final : public VNVisitor { iterateChildren(nodep); } - // FSM extraction only cares about clocked always processes. - void visit(AstAlways* nodep) override { processAlways(nodep); } + // Collect one-block FSMs immediately, strict register candidates for later + // pairing, and combinational processes for the second-stage transition + // scan. + void visit(AstAlways* nodep) override { + processOneBlockAlways(nodep); + const RegisterAlwaysAnalyzer analyzer{m_scopep}; + FsmRegisterCandidate reg; + if (analyzer.matchRegisterCandidate(nodep, reg)) { + m_registerCandidates.emplace(reg.stateVscp(), reg); + } + if (nodep->keyword() == VAlwaysKwd::ALWAYS_COMB) { + m_comboAlwayss.emplace_back(m_scopep, nodep); + } else if (nodep->keyword() == VAlwaysKwd::ALWAYS) { + if (!nodep->sentreep() || isPlainComboSentree(nodep->sentreep())) { + m_comboAlwayss.emplace_back(m_scopep, nodep); + } else { + m_nonComboAlwayss.emplace_back(m_scopep, nodep); + } + } + } // Continue the walk through the rest of the design hierarchy. void visit(AstNode* nodep) override { iterateChildren(nodep); } @@ -573,6 +1104,8 @@ public: FsmDetectVisitor(FsmState& state, AstNetlist* rootp) : m_state{state} { iterate(rootp); + for (const FsmComboAlways& combo : m_comboAlwayss) processComboAlways(combo); + for (const FsmComboAlways& combo : m_nonComboAlwayss) warnUnsupportedComboAlways(combo); } }; @@ -621,7 +1154,9 @@ class FsmLowerVisitor final { // used by generated models: declarations, previous-state tracking, and the // pre/post-triggered increment logic for states and arcs. void buildOne(const FsmGraph& graph) { - AstAlways* const alwaysp = graph.alwaysp(); + UINFO(1, "buildOne lowering FSM " << graph.stateVarName() + << " vertices=" << graph.vertices().size() << endl); + AstAlways* const alwaysp = graph.stateAlwaysp(); AstScope* const scopep = graph.scopep(); AstVarScope* const stateVscp = graph.stateVarScopep(); FileLine* const flp = graph.fileline(); @@ -761,9 +1296,7 @@ public: // still valid in the same pass. explicit FsmLowerVisitor(const FsmState& state) : m_state{state} { - for (const std::pair& it : m_state.fsms()) { - buildOne(*it.second.graphp); - } + for (const auto& it : m_state.fsms()) { buildOne(*it.second.graphp); } } }; @@ -777,7 +1310,7 @@ void V3FsmDetect::detect(AstNetlist* rootp) { FsmDetectVisitor detect{state, rootp}; if (dumpGraphLevel() >= 6) { size_t index = 0; - for (const std::pair& it : state.fsms()) { + for (const auto& it : state.fsms()) { it.second.graphp->dumpDotFilePrefixed(it.second.graphp->dumpTag(index++)); } } diff --git a/test_regress/t/t_cover_fsm_basic.py b/test_regress/t/t_cover_fsm_basic.py index 728bcebef..628914b4f 100755 --- a/test_regress/t/t_cover_fsm_basic.py +++ b/test_regress/t/t_cover_fsm_basic.py @@ -17,16 +17,6 @@ test.compile(verilator_flags2=['--cc --coverage-fsm']) test.execute() -test.run(cmd=[ - os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", - test.obj_dir + "/coverage.dat", -], - logfile=test.obj_dir + "/summary.log", - tee=False, - verilator_run=True) - -test.files_identical(test.obj_dir + "/summary.log", "t/" + test.name + "_summary.out") - test.run(cmd=[ os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", "--annotate", diff --git a/test_regress/t/t_cover_fsm_basic_summary.out b/test_regress/t/t_cover_fsm_basic_summary.out deleted file mode 100644 index a0e1606ad..000000000 --- a/test_regress/t/t_cover_fsm_basic_summary.out +++ /dev/null @@ -1,7 +0,0 @@ -Coverage Summary: - line : 0.0% (0/0) - toggle : 0.0% (0/0) - branch : 0.0% (0/0) - expr : 0.0% (0/0) - fsm_state : 50.0% (2/4) - fsm_arc : 100.0% (5/5) diff --git a/test_regress/t/t_cover_fsm_beginif.out b/test_regress/t/t_cover_fsm_beginif.out index b7aca2059..314d1b5aa 100644 --- a/test_regress/t/t_cover_fsm_beginif.out +++ b/test_regress/t/t_cover_fsm_beginif.out @@ -53,11 +53,20 @@ %000002 // [fsm_state t.state::S0] %000000 // [fsm_state t.state::S1] *** UNCOVERED *** %000003 // [fsm_state t.state::S2] - S0: - if (sel) state <= S1; - else state <= S2; - S1: state <= S0; - default: state <= S0; + S0: begin + if (sel) begin + state <= S1; + end + else begin + state <= S2; + end + end + S1: begin + state <= S0; + end + default: begin + state <= S0; + end endcase end end diff --git a/test_regress/t/t_cover_fsm_beginif.v b/test_regress/t/t_cover_fsm_beginif.v index 4ad6f1437..b42d792cc 100644 --- a/test_regress/t/t_cover_fsm_beginif.v +++ b/test_regress/t/t_cover_fsm_beginif.v @@ -44,11 +44,20 @@ module t ( end else begin case (state) - S0: - if (sel) state <= S1; - else state <= S2; - S1: state <= S0; - default: state <= S0; + S0: begin + if (sel) begin + state <= S1; + end + else begin + state <= S2; + end + end + S1: begin + state <= S0; + end + default: begin + state <= S0; + end endcase end end diff --git a/test_regress/t/t_cover_fsm_case_next_ok_multi.out b/test_regress/t/t_cover_fsm_case_next_ok_multi.out new file mode 100644 index 000000000..776f62afa --- /dev/null +++ b/test_regress/t/t_cover_fsm_case_next_ok_multi.out @@ -0,0 +1,122 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM coverage keeps grouped canonical case(state_d) forms + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + // Group accepted canonical case(state_d) forms so supported next-state + // defaults stay separate from the rejected wrong-RHS and tail-mismatch shapes. + + module fsm_case_next_other_assign_ok ( + input logic clk, + input logic rst, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'b00, + S1 = 2'b01, + S2 = 2'b10 + } state_t; + + logic [1:0] aux; + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + + initial aux = 2'b00; + + always_comb begin + aux[0] = start; + state_d = state_q; +%000004 case (state_d) + // [FSM coverage] +%000001 // [fsm_arc t.case_next_other_assign_ok_u.state_q::ANY->S0[reset_include]] [reset arc, excluded from %] +%000000 // [fsm_arc t.case_next_other_assign_ok_u.state_q::S0->S1] +%000004 // [fsm_arc t.case_next_other_assign_ok_u.state_q::S0->S2] +%000003 // [fsm_state t.case_next_other_assign_ok_u.state_q::S0] +%000000 // [fsm_state t.case_next_other_assign_ok_u.state_q::S1] *** UNCOVERED *** +%000004 // [fsm_state t.case_next_other_assign_ok_u.state_q::S2] + S0: state_d = start ? S1 : S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) begin + state_q <= S0; + end else begin + state_q <= state_d; + end + end + endmodule + + module fsm_case_next_other_lhs_ok ( + input logic clk, + input logic rst, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'b00, + S1 = 2'b01, + S2 = 2'b10 + } state_t; + + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + state_t other_d; + + always_comb begin + other_d = state_q; + state_d = state_q; +%000004 case (state_d) + // [FSM coverage] +%000001 // [fsm_arc t.case_next_other_lhs_ok_u.state_q::ANY->S0[reset_include]] [reset arc, excluded from %] +%000000 // [fsm_arc t.case_next_other_lhs_ok_u.state_q::S0->S1] +%000004 // [fsm_arc t.case_next_other_lhs_ok_u.state_q::S0->S2] +%000003 // [fsm_state t.case_next_other_lhs_ok_u.state_q::S0] +%000000 // [fsm_state t.case_next_other_lhs_ok_u.state_q::S1] *** UNCOVERED *** +%000004 // [fsm_state t.case_next_other_lhs_ok_u.state_q::S2] + S0: state_d = start ? S1 : S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) begin + state_q <= S0; + end else begin + state_q <= state_d; + end + end + endmodule + + module t ( + input logic clk + ); + logic rst; + logic start; + integer cyc; + + fsm_case_next_other_assign_ok case_next_other_assign_ok_u ( + .clk(clk), .rst(rst), .start(start)); + fsm_case_next_other_lhs_ok case_next_other_lhs_ok_u ( + .clk(clk), .rst(rst), .start(start)); + + initial begin + rst = 1'b1; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + endmodule + diff --git a/test_regress/t/t_cover_fsm_case_next_ok_multi.py b/test_regress/t/t_cover_fsm_case_next_ok_multi.py new file mode 100755 index 000000000..a95c8efad --- /dev/null +++ b/test_regress/t/t_cover_fsm_case_next_ok_multi.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage keeps grouped canonical case(state_d) forms +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import os + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--cc --coverage-fsm']) + +test.execute() + +test.run(cmd=[ + os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--annotate", + test.obj_dir + "/annotated", + test.obj_dir + "/coverage.dat", +], + verilator_run=True) + +test.run(cmd=[ + os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--include-reset-arcs", + "--annotate", + test.obj_dir + "/annotated-incl", + test.obj_dir + "/coverage.dat", +], + verilator_run=True) + +test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) +test.files_identical(test.obj_dir + "/annotated-incl/" + test.name + ".v", + "t/" + test.name + "_incl.out") + +test.passes() diff --git a/test_regress/t/t_cover_fsm_case_next_ok_multi.v b/test_regress/t/t_cover_fsm_case_next_ok_multi.v new file mode 100644 index 000000000..ac818c228 --- /dev/null +++ b/test_regress/t/t_cover_fsm_case_next_ok_multi.v @@ -0,0 +1,106 @@ +// DESCRIPTION: Verilator: FSM coverage keeps grouped canonical case(state_d) forms +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// Group accepted canonical case(state_d) forms so supported next-state +// defaults stay separate from the rejected wrong-RHS and tail-mismatch shapes. + +module fsm_case_next_other_assign_ok ( + input logic clk, + input logic rst, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'b00, + S1 = 2'b01, + S2 = 2'b10 + } state_t; + + logic [1:0] aux; + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + + initial aux = 2'b00; + + always_comb begin + aux[0] = start; + state_d = state_q; + case (state_d) + S0: state_d = start ? S1 : S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) begin + state_q <= S0; + end else begin + state_q <= state_d; + end + end +endmodule + +module fsm_case_next_other_lhs_ok ( + input logic clk, + input logic rst, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'b00, + S1 = 2'b01, + S2 = 2'b10 + } state_t; + + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + state_t other_d; + + always_comb begin + other_d = state_q; + state_d = state_q; + case (state_d) + S0: state_d = start ? S1 : S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) begin + state_q <= S0; + end else begin + state_q <= state_d; + end + end +endmodule + +module t ( + input logic clk +); + logic rst; + logic start; + integer cyc; + + fsm_case_next_other_assign_ok case_next_other_assign_ok_u ( + .clk(clk), .rst(rst), .start(start)); + fsm_case_next_other_lhs_ok case_next_other_lhs_ok_u ( + .clk(clk), .rst(rst), .start(start)); + + initial begin + rst = 1'b1; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end +endmodule diff --git a/test_regress/t/t_cover_fsm_case_next_ok_multi_incl.out b/test_regress/t/t_cover_fsm_case_next_ok_multi_incl.out new file mode 100644 index 000000000..1b3f91613 --- /dev/null +++ b/test_regress/t/t_cover_fsm_case_next_ok_multi_incl.out @@ -0,0 +1,122 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM coverage keeps grouped canonical case(state_d) forms + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + // Group accepted canonical case(state_d) forms so supported next-state + // defaults stay separate from the rejected wrong-RHS and tail-mismatch shapes. + + module fsm_case_next_other_assign_ok ( + input logic clk, + input logic rst, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'b00, + S1 = 2'b01, + S2 = 2'b10 + } state_t; + + logic [1:0] aux; + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + + initial aux = 2'b00; + + always_comb begin + aux[0] = start; + state_d = state_q; +%000004 case (state_d) + // [FSM coverage] +%000001 // [fsm_arc t.case_next_other_assign_ok_u.state_q::ANY->S0[reset_include]] +%000000 // [fsm_arc t.case_next_other_assign_ok_u.state_q::S0->S1] +%000004 // [fsm_arc t.case_next_other_assign_ok_u.state_q::S0->S2] +%000003 // [fsm_state t.case_next_other_assign_ok_u.state_q::S0] +%000000 // [fsm_state t.case_next_other_assign_ok_u.state_q::S1] *** UNCOVERED *** +%000004 // [fsm_state t.case_next_other_assign_ok_u.state_q::S2] + S0: state_d = start ? S1 : S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) begin + state_q <= S0; + end else begin + state_q <= state_d; + end + end + endmodule + + module fsm_case_next_other_lhs_ok ( + input logic clk, + input logic rst, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'b00, + S1 = 2'b01, + S2 = 2'b10 + } state_t; + + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + state_t other_d; + + always_comb begin + other_d = state_q; + state_d = state_q; +%000004 case (state_d) + // [FSM coverage] +%000001 // [fsm_arc t.case_next_other_lhs_ok_u.state_q::ANY->S0[reset_include]] +%000000 // [fsm_arc t.case_next_other_lhs_ok_u.state_q::S0->S1] +%000004 // [fsm_arc t.case_next_other_lhs_ok_u.state_q::S0->S2] +%000003 // [fsm_state t.case_next_other_lhs_ok_u.state_q::S0] +%000000 // [fsm_state t.case_next_other_lhs_ok_u.state_q::S1] *** UNCOVERED *** +%000004 // [fsm_state t.case_next_other_lhs_ok_u.state_q::S2] + S0: state_d = start ? S1 : S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) begin + state_q <= S0; + end else begin + state_q <= state_d; + end + end + endmodule + + module t ( + input logic clk + ); + logic rst; + logic start; + integer cyc; + + fsm_case_next_other_assign_ok case_next_other_assign_ok_u ( + .clk(clk), .rst(rst), .start(start)); + fsm_case_next_other_lhs_ok case_next_other_lhs_ok_u ( + .clk(clk), .rst(rst), .start(start)); + + initial begin + rst = 1'b1; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + endmodule + diff --git a/test_regress/t/t_cover_fsm_combo_same_warn_bad.out b/test_regress/t/t_cover_fsm_combo_same_warn_bad.out new file mode 100644 index 000000000..32f725885 --- /dev/null +++ b/test_regress/t/t_cover_fsm_combo_same_warn_bad.out @@ -0,0 +1,9 @@ +%Warning-COVERIGN: t/t_cover_fsm_combo_same_warn_bad.v:28:19: Ignoring unsupported: FSM coverage on multiple supported case statements found in the same combinational always block. Only the first candidate will be instrumented. + 28 | S1: state_d = S2; + | ^ + t/t_cover_fsm_combo_same_warn_bad.v:24:19: ... Location of first supported candidate for 't.state_q' + 24 | S0: state_d = S1; + | ^ + ... For warning description see https://verilator.org/warn/COVERIGN?v=latest + ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. +%Error: Exiting due to diff --git a/test_regress/t/t_cover_fsm_combo_same_warn_bad.py b/test_regress/t/t_cover_fsm_combo_same_warn_bad.py new file mode 100755 index 000000000..46cc6db99 --- /dev/null +++ b/test_regress/t/t_cover_fsm_combo_same_warn_bad.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: same-FSM combo multi-case warning test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +# Two supported case statements in the same combinational always block for the +# same FSM are legal RTL, but Phase 1 only instruments the first and warns on +# the later duplicate. +test.compile(verilator_flags2=["--cc --coverage"], + fails=True, + expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_cover_fsm_combo_same_warn_bad.v b/test_regress/t/t_cover_fsm_combo_same_warn_bad.v new file mode 100644 index 000000000..fa6966182 --- /dev/null +++ b/test_regress/t/t_cover_fsm_combo_same_warn_bad.v @@ -0,0 +1,38 @@ +// DESCRIPTION: Verilator: same-FSM combo multi-case warning test +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input logic clk, + input logic rst +); + + typedef enum logic [1:0] { + S0, + S1, + S2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + case (state_q) + S0: state_d = S1; + default: ; + endcase + case (state_q) + S1: state_d = S2; + default: ; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end + +endmodule diff --git a/test_regress/t/t_cover_fsm_decldump.py b/test_regress/t/t_cover_fsm_decldump.py index dc2cd087d..66c75caa2 100755 --- a/test_regress/t/t_cover_fsm_decldump.py +++ b/test_regress/t/t_cover_fsm_decldump.py @@ -12,7 +12,7 @@ from pathlib import Path import vltest_bootstrap test.scenarios('vlt') -test.top_filename = "t/t_cover_fsm_styles.v" +test.top_filename = "t/t_cover_fsm_policy_accept_multi.v" # Dump the lowered AST so AstCoverOtherDecl::dump() sees FSM metadata-bearing # coverage declarations directly. This avoids JSON/schema coupling while still @@ -22,7 +22,7 @@ test.lint(v_flags=["--coverage-fsm", "--dump-tree"]) tree_files = [Path(filename) for filename in test.glob_some(test.obj_dir + "/*.tree")] tree_texts = [filename.read_text(encoding="utf8") for filename in tree_files] -assert any("COVEROTHERDECL" in text and " fv=t.state" in text for text in tree_texts) +assert any("COVEROTHERDECL" in text and " fv=t.style_u.state" in text for text in tree_texts) assert any( "COVEROTHERDECL" in text and " ff=ANY" in text and " ft=S0" in text and " fg=reset" in text for text in tree_texts) diff --git a/test_regress/t/t_cover_fsm_enum_bad.out b/test_regress/t/t_cover_fsm_enum_bad.out deleted file mode 100644 index 72de142b4..000000000 --- a/test_regress/t/t_cover_fsm_enum_bad.out +++ /dev/null @@ -1,6 +0,0 @@ -%Warning-COVERIGN: t/t_cover_fsm_enum_bad.v:29:19: Ignoring unsupported: FSM coverage on enum state transitions that assign a constant not present in the declared enum - 29 | S0: state <= 2'd3; - | ^~ - ... For warning description see https://verilator.org/warn/COVERIGN?v=latest - ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. -%Error: Exiting due to diff --git a/test_regress/t/t_cover_fsm_enum_bad.v b/test_regress/t/t_cover_fsm_enum_bad.v deleted file mode 100644 index cd7ac7442..000000000 --- a/test_regress/t/t_cover_fsm_enum_bad.v +++ /dev/null @@ -1,36 +0,0 @@ -// DESCRIPTION: Verilator: FSM enum transition rejects unknown constant values -// -// This file ONLY is placed under the Creative Commons Public Domain. -// SPDX-FileCopyrightText: 2026 Wilson Snyder -// SPDX-License-Identifier: CC0-1.0 - -module t ( - input logic clk, - input logic rst -); - - typedef enum logic [1:0] { - S0, - S1 - } state_t; - - state_t state; - - // FSM coverage should reject a constant next-state value that is not one of - // the declared enum items. This keeps graph construction aligned with the - // enum-backed state set instead of silently dropping the transition. - always_ff @(posedge clk) begin - if (rst) begin - state <= S0; - end - else begin - case (state) - /* verilator lint_off ENUMVALUE */ - S0: state <= 2'd3; - /* verilator lint_on ENUMVALUE */ - default: state <= S0; - endcase - end - end - -endmodule diff --git a/test_regress/t/t_cover_fsm_forced.out b/test_regress/t/t_cover_fsm_forced.out deleted file mode 100644 index e0112cee6..000000000 --- a/test_regress/t/t_cover_fsm_forced.out +++ /dev/null @@ -1,52 +0,0 @@ -// // verilator_coverage annotation - // DESCRIPTION: Verilator: FSM coverage forced non-enum test - // - // This file ONLY is placed under the Creative Commons Public Domain. - // SPDX-FileCopyrightText: 2026 Wilson Snyder - // SPDX-License-Identifier: CC0-1.0 - - module t ( - input clk - ); - - integer cyc; - logic rst; - logic [1:0] state /*verilator fsm_state*/; - - initial begin - cyc = 0; - rst = 1'b1; - end - - always @(posedge clk) begin - cyc <= cyc + 1; - if (cyc == 1) rst <= 1'b0; - if (cyc == 6) begin - $write("*-* All Finished *-*\n"); - $finish; - end - end - - always_ff @(posedge clk) begin - if (rst) begin - state <= 2'd0; - end - else begin -%000002 case (state) - // [FSM coverage] -%000001 // [fsm_arc t.state::ANY->S0[reset]] [reset arc, excluded from %] -%000002 // [fsm_arc t.state::S0->S1] -%000002 // [fsm_arc t.state::S1->S2] -%000001 // [fsm_state t.state::S0] -%000002 // [fsm_state t.state::S1] -%000002 // [fsm_state t.state::S2] -%000000 // [fsm_state t.state::S3] *** UNCOVERED *** - 2'd0: state <= 2'd1; - 2'd1: state <= 2'd2; - default: state <= 2'd0; - endcase - end - end - - endmodule - diff --git a/test_regress/t/t_cover_fsm_forced.v b/test_regress/t/t_cover_fsm_forced.v deleted file mode 100644 index 974aee868..000000000 --- a/test_regress/t/t_cover_fsm_forced.v +++ /dev/null @@ -1,42 +0,0 @@ -// DESCRIPTION: Verilator: FSM coverage forced non-enum test -// -// This file ONLY is placed under the Creative Commons Public Domain. -// SPDX-FileCopyrightText: 2026 Wilson Snyder -// SPDX-License-Identifier: CC0-1.0 - -module t ( - input clk -); - - integer cyc; - logic rst; - logic [1:0] state /*verilator fsm_state*/; - - initial begin - cyc = 0; - rst = 1'b1; - end - - always @(posedge clk) begin - cyc <= cyc + 1; - if (cyc == 1) rst <= 1'b0; - if (cyc == 6) begin - $write("*-* All Finished *-*\n"); - $finish; - end - end - - always_ff @(posedge clk) begin - if (rst) begin - state <= 2'd0; - end - else begin - case (state) - 2'd0: state <= 2'd1; - 2'd1: state <= 2'd2; - default: state <= 2'd0; - endcase - end - end - -endmodule diff --git a/test_regress/t/t_cover_fsm_graphdump.py b/test_regress/t/t_cover_fsm_graphdump.py index 3290be13f..9742f2aaf 100755 --- a/test_regress/t/t_cover_fsm_graphdump.py +++ b/test_regress/t/t_cover_fsm_graphdump.py @@ -10,7 +10,7 @@ import vltest_bootstrap test.scenarios('vltmt') -test.top_filename = "t/t_cover_fsm_styles.v" +test.top_filename = "t/t_cover_fsm_policy_accept_multi.v" test.compile(v_flags2=["--coverage-fsm", "--dumpi-graph", "6"], threads=2) diff --git a/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.out b/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.out new file mode 100644 index 000000000..ba8106741 --- /dev/null +++ b/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.out @@ -0,0 +1,15 @@ +%Warning-COVERIGN: t/t_cover_fsm_if_unknown_enum_multi_bad.v:26:19: Ignoring unsupported: FSM coverage on enum state transitions that assign a constant that is not present in the declared enum + 26 | S0: state_d = sel ? 2'd3 : S1; + | ^ + ... For warning description see https://verilator.org/warn/COVERIGN?v=latest + ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. +%Warning-COVERIGN: t/t_cover_fsm_if_unknown_enum_multi_bad.v:53:19: Ignoring unsupported: FSM coverage on enum state transitions that assign a constant that is not present in the declared enum + 53 | S0: state_d = sel ? S1 : 2'd3; + | ^ +%Warning-COVERIGN: t/t_cover_fsm_if_unknown_enum_multi_bad.v:79:19: Ignoring unsupported: FSM coverage on enum state transitions that assign a constant that is not present in the declared enum + 79 | S0: state_d = 2'd3; + | ^ +%Warning-COVERIGN: t/t_cover_fsm_if_unknown_enum_multi_bad.v:126:15: Ignoring unsupported: FSM coverage on enum state transitions that assign a constant that is not present in the declared enum + 126 | state_q <= 2'd3; + | ^~ +%Error: Exiting due to diff --git a/test_regress/t/t_cover_fsm_enum_bad.py b/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.py similarity index 63% rename from test_regress/t/t_cover_fsm_enum_bad.py rename to test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.py index 541252ec8..936ca5648 100755 --- a/test_regress/t/t_cover_fsm_enum_bad.py +++ b/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# DESCRIPTION: Verilator: FSM enum transition bad-value test +# DESCRIPTION: Verilator: FSM direct and conditional transition bad enum tests # # This program is free software; you can redistribute it and/or modify it # under the terms of either the GNU Lesser General Public License Version 3 @@ -11,9 +11,6 @@ import vltest_bootstrap test.scenarios('vlt') -# When an enum-backed FSM assigns a constant that is not one of the declared -# enum items, FSM coverage should warn and skip the unsupported edge rather -# than turning optional coverage into a hard compile failure. test.lint(verilator_flags2=["--coverage-fsm"], fails=True, expect_filename=test.golden_filename) test.passes() diff --git a/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.v b/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.v new file mode 100644 index 000000000..981f9f5e1 --- /dev/null +++ b/test_regress/t/t_cover_fsm_if_unknown_enum_multi_bad.v @@ -0,0 +1,140 @@ +// DESCRIPTION: Verilator: FSM rejects unknown enum constants in direct and conditional transitions +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// Group the same unsupported warning family across direct, conditional, and +// reset-driven enum assignments that use constants outside the declared enum. + +module unknown_then ( + input logic clk +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + + logic sel; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + + always_comb begin + state_d = state_q; + case (state_q) + /* verilator lint_off ENUMVALUE */ + S0: state_d = sel ? 2'd3 : S1; + /* verilator lint_on ENUMVALUE */ + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + state_q <= state_d; + end +endmodule + +module unknown_else ( + input logic clk +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + + logic sel; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + + always_comb begin + state_d = state_q; + case (state_q) + /* verilator lint_off ENUMVALUE */ + S0: state_d = sel ? S1 : 2'd3; + /* verilator lint_on ENUMVALUE */ + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + state_q <= state_d; + end +endmodule + +module unknown_direct ( + input logic clk +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + + state_t state_q /*verilator fsm_state*/; + state_t state_d; + + always_comb begin + state_d = state_q; + case (state_q) + /* verilator lint_off ENUMVALUE */ + S0: state_d = 2'd3; + /* verilator lint_on ENUMVALUE */ + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + state_q <= state_d; + end +endmodule + +module unknown_reset ( + input logic clk +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + + logic rst; + integer cyc; + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + + initial begin + rst = 1'b1; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + end + + always_comb begin + state_d = state_q; + case (state_q) + S0: state_d = S1; + default: state_d = S0; + endcase + end + + // Reset-arc extraction uses a separate path from steady-state case-item arc + // extraction, so keep one reset-only bad enum target in this grouped test. + always_ff @(posedge clk) begin + if (rst) begin + /* verilator lint_off ENUMVALUE */ + state_q <= 2'd3; + /* verilator lint_on ENUMVALUE */ + end else begin + state_q <= state_d; + end + end +endmodule + +module t; + logic clk; + unknown_then unknown_then_u(.clk(clk)); + unknown_else unknown_else_u(.clk(clk)); + unknown_direct unknown_direct_u(.clk(clk)); + unknown_reset unknown_reset_u(.clk(clk)); +endmodule diff --git a/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.out b/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.out new file mode 100644 index 000000000..71f746138 --- /dev/null +++ b/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.out @@ -0,0 +1,6 @@ +%Warning-COVERIGN: t/t_cover_fsm_nextstate_overwrite_warn.v:21:13: Ignoring unsupported: FSM coverage on case('t.state_d') when the canonical 't.state_d' = 't.state_q' default is overwritten before the case statement + 21 | state_d = (state_d == S0) ? S1 : S0; + | ^ + ... For warning description see https://verilator.org/warn/COVERIGN?v=latest + ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. +%Error: Exiting due to diff --git a/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.py b/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.py new file mode 100755 index 000000000..40fb48177 --- /dev/null +++ b/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM case(state_d) overwritten-default warning test +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +# A canonical `state_d = state_q` default followed by another top-level write +# to `state_d` before `case (state_d)` is legal RTL, but it is not the narrow +# Phase 1 shape we support. Warn and reject rather than silently skipping it. +test.compile(verilator_flags2=["--cc --coverage"], + fails=True, + expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.v b/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.v new file mode 100644 index 000000000..1e13f5570 --- /dev/null +++ b/test_regress/t/t_cover_fsm_nextstate_overwrite_warn.v @@ -0,0 +1,32 @@ +// DESCRIPTION: Verilator: FSM case(state_d) overwritten-default warning test +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input logic clk, + input logic rst +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + state_d = (state_d == S0) ? S1 : S0; + case (state_d) + S0: state_d = S1; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule diff --git a/test_regress/t/t_cover_fsm_plain_always_ignore_multi.out b/test_regress/t/t_cover_fsm_plain_always_ignore_multi.out new file mode 100644 index 000000000..8b8f255e5 --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_ignore_multi.out @@ -0,0 +1 @@ +# SystemC::Coverage-3 diff --git a/test_regress/t/t_cover_fsm_plain_always_ignore_multi.py b/test_regress/t/t_cover_fsm_plain_always_ignore_multi.py new file mode 100755 index 000000000..1b2d7e17a --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_ignore_multi.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage ignores grouped non-matching plain always shapes +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--cc --coverage-fsm', '-Werror-COVERIGN']) + +test.execute() + +test.files_identical(test.obj_dir + "/coverage.dat", "t/" + test.name + ".out") + +test.passes() diff --git a/test_regress/t/t_cover_fsm_plain_always_ignore_multi.v b/test_regress/t/t_cover_fsm_plain_always_ignore_multi.v new file mode 100644 index 000000000..ed04b539d --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_ignore_multi.v @@ -0,0 +1,99 @@ +// DESCRIPTION: Verilator: FSM coverage ignores non-matching plain always scan shapes +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// Group plain-always warning-scan shapes that should be ignored silently +// before the detector ever decides an FSM candidate is present. + +module ignore_sel_expr ( + input logic clk, + input logic rst, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + + // Plain always with a non-VarRef selector should be ignored silently. + always @(posedge clk) begin + state_d = state_q; + case (state_q + 1) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule + +module ignore_other_selector ( + input logic clk, + input logic rst, + input logic start, + input logic other +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + + // Plain always with an unrelated selector should also be ignored silently. + always @(posedge clk) begin + state_d = state_q; + case (other) + 1'b0: state_d = start ? S1 : S0; + default: state_d = S2; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule + +module t ( + input logic clk +); + logic rst; + logic start; + logic other; + integer cyc; + + ignore_sel_expr ignore_sel_expr_u(.*); + ignore_other_selector ignore_other_selector_u(.*); + + initial begin + rst = 1'b1; + start = 1'b0; + other = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 4) other <= 1'b1; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end +endmodule diff --git a/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.out b/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.out new file mode 100644 index 000000000..064ef2ff1 --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.out @@ -0,0 +1,12 @@ +%Warning-COVERIGN: t/t_cover_fsm_plain_always_warn_multi_bad.v:25:5: Ignoring unsupported: FSM coverage on non-clocked always blocks requires a combinational sensitivity list or always_comb + 25 | case (state_q) + | ^~~~ + ... For warning description see https://verilator.org/warn/COVERIGN?v=latest + ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. +%Warning-COVERIGN: t/t_cover_fsm_plain_always_warn_multi_bad.v:55:5: Ignoring unsupported: FSM coverage on non-clocked always blocks requires a combinational sensitivity list or always_comb + 55 | case (state_d) + | ^~~~ +%Warning-COVERIGN: t/t_cover_fsm_plain_always_warn_multi_bad.v:86:5: Ignoring unsupported: FSM coverage on non-clocked always blocks requires a combinational sensitivity list or always_comb + 86 | case (state_q) + | ^~~~ +%Error: Exiting due to diff --git a/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.py b/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.py new file mode 100755 index 000000000..57716476b --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage warns on grouped non-clocked always near-FSM shapes +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.compile(verilator_flags2=['--cc --coverage-fsm'], + fails=True, + expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.v b/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.v new file mode 100644 index 000000000..8194799f6 --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_warn_multi_bad.v @@ -0,0 +1,106 @@ +// DESCRIPTION: Verilator: FSM coverage warns on non-clocked always near-FSM shapes +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module warn_edge ( + input logic clk, + input logic rst, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + + // Plain edge-sensitive next-state logic should warn even when the selector + // is the canonical state_q form. + always @(posedge clk) begin + state_d = state_q; + case (state_q) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule + +module warn_case_next ( + input logic clk, + input logic rst, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + + // Plain always also warns for the near-supported case(state_d) style. + always @(posedge clk) begin + state_d = state_q; + case (state_d) + S0: state_d = start ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule + +module warn_default_incl ( + input logic clk, + input logic rst, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q /*verilator fsm_reset_arc*/ /*verilator fsm_arc_include_cond*/; + state_t state_d; + + // default becomes supported only because include_cond is set, but the block + // still warns because it is a plain edge-sensitive always. + always @(posedge clk) begin + state_d = state_q; + case (state_q) + default: state_d = S0; + S0: state_d = start ? state_q : state_q; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule + +module t; + logic clk; + logic rst; + logic start; + + warn_edge warn_edge_u(.*); + warn_case_next warn_case_next_u(.*); + warn_default_incl warn_default_incl_u(.*); +endmodule diff --git a/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.out b/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.out new file mode 100644 index 000000000..05101a413 --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.out @@ -0,0 +1,9 @@ +# SystemC::Coverage-3 +C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl28n5tfsm_arcpagev_fsm_arc/$rootot.near_canonical_state_d_case_u.state_d::S0->S1Fvt.near_canonical_state_d_case_u.state_dFfS0FtS1htop.TOP' 0 +C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl28n5tfsm_arcpagev_fsm_arc/$rootot.near_canonical_state_d_case_u.state_d::S0->S2Fvt.near_canonical_state_d_case_u.state_dFfS0FtS2htop.TOP' 0 +C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl28n5tfsm_statepagev_fsm_state/$rootot.near_canonical_state_d_case_u.state_d::S0Fvt.near_canonical_state_d_case_u.state_dFtS0htop.TOP' 0 +C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl28n5tfsm_statepagev_fsm_state/$rootot.near_canonical_state_d_case_u.state_d::S1Fvt.near_canonical_state_d_case_u.state_dFtS1htop.TOP' 0 +C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl28n5tfsm_statepagev_fsm_state/$rootot.near_canonical_state_d_case_u.state_d::S2Fvt.near_canonical_state_d_case_u.state_dFtS2htop.TOP' 0 +C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl60n5tfsm_statepagev_fsm_state/$rootot.selector_matches_noassign_u.state_q::S0Fvt.selector_matches_noassign_u.state_qFtS0htop.TOP' 0 +C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl60n5tfsm_statepagev_fsm_state/$rootot.selector_matches_noassign_u.state_q::S1Fvt.selector_matches_noassign_u.state_qFtS1htop.TOP' 0 +C 'ft/t_cover_fsm_plain_always_zerohit_multi.vl60n5tfsm_statepagev_fsm_state/$rootot.selector_matches_noassign_u.state_q::S2Fvt.selector_matches_noassign_u.state_qFtS2htop.TOP' 0 diff --git a/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.py b/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.py new file mode 100644 index 000000000..163436db8 --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage keeps zero-hit records for near-miss plain always shapes +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--cc --coverage-fsm', '-Werror-COVERIGN']) + +test.execute() + +test.files_identical(test.obj_dir + "/coverage.dat", "t/" + test.name + ".out") + +test.passes() diff --git a/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.v b/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.v new file mode 100644 index 000000000..d8b63f3fb --- /dev/null +++ b/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.v @@ -0,0 +1,98 @@ +// DESCRIPTION: Verilator: FSM coverage keeps zero-hit records for near-miss plain always shapes +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// Group plain-always near misses that still recover enough structure to leave +// zero-hit FSM records behind, but must not warn or model a supported FSM. + +module near_canonical_state_d_case ( + input logic clk, + input logic rst, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + + // Unsupported for the plain-always warning scan: case(state_d) is only + // near-supported when it follows the canonical state_d = state_q default. + always @(posedge clk) begin + state_d = S1; + case (state_d) + S0: state_d = start ? S1 : S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule + +module selector_matches_noassign ( + input logic clk, + input logic rst, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + logic other; + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + + // The selector matches the FSM state, but the case body never assigns + // state_d, so the warning scan should ignore it while leaving zero-hit + // state points behind. + always @(posedge clk) begin + state_d = state_q; + case (state_q) + S0: other = start; + default: other = 1'b0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule + +module t ( + input logic clk +); + logic rst; + logic start; + integer cyc; + + near_canonical_state_d_case near_canonical_state_d_case_u(.*); + selector_matches_noassign selector_matches_noassign_u(.*); + + initial begin + rst = 1'b1; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end +endmodule diff --git a/test_regress/t/t_cover_fsm_policy_accept_multi.out b/test_regress/t/t_cover_fsm_policy_accept_multi.out new file mode 100644 index 000000000..c2c35da00 --- /dev/null +++ b/test_regress/t/t_cover_fsm_policy_accept_multi.out @@ -0,0 +1,141 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM coverage keeps grouped accepted policy-style forms + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + // Group accepted policy-driven forms that should stay inferred, even though + // they exercise different coverage attributes or non-enum state policies. + + module fsm_style_incl ( + input logic clk, + input logic rst, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2, + S3 = 2'd3 + } state_t; + + state_t state /*verilator fsm_arc_include_cond*/; + + always_ff @(posedge clk) begin + if (rst) begin + state <= S0; + end else begin +%000003 case (state) + // [FSM coverage] +%000001 // [fsm_arc t.style_u.state::ANY->S0[reset]] [reset arc, excluded from %] +%000000 // [fsm_arc t.style_u.state::S0->S1] +%000003 // [fsm_arc t.style_u.state::S0->S2] +%000000 // [fsm_arc t.style_u.state::S1->S3] +%000000 // [SYNTHETIC DEFAULT ARC: t.style_u.state::default->S0] +%000002 // [fsm_state t.style_u.state::S0] +%000000 // [fsm_state t.style_u.state::S1] *** UNCOVERED *** +%000003 // [fsm_state t.style_u.state::S2] +%000000 // [fsm_state t.style_u.state::S3] *** UNCOVERED *** + S0: + if (start) state <= S1; + else state <= S2; + S1: state <= S3; + default: state <= S0; + endcase + end + end + endmodule + + module fsm_default_incl_ok ( + input logic clk, + input logic rst, + input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q /*verilator fsm_arc_include_cond*/; + state_t state_d; + + always_comb begin + state_d = state_q; +%000003 case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.default_incl_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000000 // [fsm_arc t.default_incl_u.state_q::S0->S1] +%000003 // [fsm_arc t.default_incl_u.state_q::S0->S2] +%000000 // [SYNTHETIC DEFAULT ARC: t.default_incl_u.state_q::default->S0] +%000002 // [fsm_state t.default_incl_u.state_q::S0] +%000000 // [fsm_state t.default_incl_u.state_q::S1] *** UNCOVERED *** +%000003 // [fsm_state t.default_incl_u.state_q::S2] + S0: state_d = start ? S1 : S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end + endmodule + + module fsm_forced_ok ( + input logic clk, + input logic rst + ); + logic [1:0] state /*verilator fsm_state*/; + + always_ff @(posedge clk) begin + if (rst) begin + state <= 2'd0; + end else begin +%000002 case (state) + // [FSM coverage] +%000001 // [fsm_arc t.forced_u.state::ANY->S0[reset]] [reset arc, excluded from %] +%000002 // [fsm_arc t.forced_u.state::S0->S1] +%000002 // [fsm_arc t.forced_u.state::S1->S2] +%000001 // [fsm_state t.forced_u.state::S0] +%000002 // [fsm_state t.forced_u.state::S1] +%000002 // [fsm_state t.forced_u.state::S2] +%000000 // [fsm_state t.forced_u.state::S3] *** UNCOVERED *** + 2'd0: state <= 2'd1; + 2'd1: state <= 2'd2; + default: state <= 2'd0; + endcase + end + end + endmodule + + module t ( + input logic clk + ); + integer cyc; + logic rst; + logic start; + + fsm_style_incl style_u (.clk(clk), .rst(rst), .start(start)); + fsm_default_incl_ok default_incl_u (.clk(clk), .rst(rst), .start(start)); + fsm_forced_ok forced_u (.clk(clk), .rst(rst)); + + initial begin + cyc = 0; + rst = 1'b1; + start = 1'b0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 6) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + endmodule + diff --git a/test_regress/t/t_cover_fsm_forced.py b/test_regress/t/t_cover_fsm_policy_accept_multi.py similarity index 78% rename from test_regress/t/t_cover_fsm_forced.py rename to test_regress/t/t_cover_fsm_policy_accept_multi.py index f368d9cf9..1b075a66f 100755 --- a/test_regress/t/t_cover_fsm_forced.py +++ b/test_regress/t/t_cover_fsm_policy_accept_multi.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# DESCRIPTION: Verilator: FSM coverage forced non-enum test +# DESCRIPTION: Verilator: FSM coverage keeps grouped accepted policy-style forms # # This program is free software; you can redistribute it and/or modify it # under the terms of either the GNU Lesser General Public License Version 3 @@ -17,8 +17,6 @@ test.compile(verilator_flags2=['--cc --coverage-fsm']) test.execute() -# Use annotated-source golden output so hit-count regressions are visible in the -# expected file instead of being hidden behind coarse coverage.dat greps. test.run(cmd=[ os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", "--annotate", diff --git a/test_regress/t/t_cover_fsm_policy_accept_multi.v b/test_regress/t/t_cover_fsm_policy_accept_multi.v new file mode 100644 index 000000000..ecd6ac21a --- /dev/null +++ b/test_regress/t/t_cover_fsm_policy_accept_multi.v @@ -0,0 +1,113 @@ +// DESCRIPTION: Verilator: FSM coverage keeps grouped accepted policy-style forms +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// Group accepted policy-driven forms that should stay inferred, even though +// they exercise different coverage attributes or non-enum state policies. + +module fsm_style_incl ( + input logic clk, + input logic rst, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2, + S3 = 2'd3 + } state_t; + + state_t state /*verilator fsm_arc_include_cond*/; + + always_ff @(posedge clk) begin + if (rst) begin + state <= S0; + end else begin + case (state) + S0: + if (start) state <= S1; + else state <= S2; + S1: state <= S3; + default: state <= S0; + endcase + end + end +endmodule + +module fsm_default_incl_ok ( + input logic clk, + input logic rst, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q /*verilator fsm_arc_include_cond*/; + state_t state_d; + + always_comb begin + state_d = state_q; + case (state_q) + S0: state_d = start ? S1 : S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule + +module fsm_forced_ok ( + input logic clk, + input logic rst +); + logic [1:0] state /*verilator fsm_state*/; + + always_ff @(posedge clk) begin + if (rst) begin + state <= 2'd0; + end else begin + case (state) + 2'd0: state <= 2'd1; + 2'd1: state <= 2'd2; + default: state <= 2'd0; + endcase + end + end +endmodule + +module t ( + input logic clk +); + integer cyc; + logic rst; + logic start; + + fsm_style_incl style_u (.clk(clk), .rst(rst), .start(start)); + fsm_default_incl_ok default_incl_u (.clk(clk), .rst(rst), .start(start)); + fsm_forced_ok forced_u (.clk(clk), .rst(rst)); + + initial begin + cyc = 0; + rst = 1'b1; + start = 1'b0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 6) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end +endmodule diff --git a/test_regress/t/t_cover_fsm_reset.out b/test_regress/t/t_cover_fsm_reset.out index ddbd755f8..8e7ec3c61 100644 --- a/test_regress/t/t_cover_fsm_reset.out +++ b/test_regress/t/t_cover_fsm_reset.out @@ -1,12 +1,16 @@ // // verilator_coverage annotation - // DESCRIPTION: Verilator: FSM coverage reset policy test + // DESCRIPTION: Verilator: FSM coverage grouped reset semantics test // // This file ONLY is placed under the Creative Commons Public Domain. // SPDX-FileCopyrightText: 2026 Wilson Snyder // SPDX-License-Identifier: CC0-1.0 - module t ( -%000006 input clk + // Group accepted simulator-style reset semantics, including reset include / + // exclude behavior and nearby fallback cases that should still preserve FSMs. + + module fsm_reset_policy ( + input logic clk, + input logic rst ); typedef enum logic [0:0] { @@ -14,51 +18,179 @@ S1 = 1'b1 } state_t; -%000001 logic rst; - integer cyc; -%000001 state_t state_incl /*verilator fsm_reset_arc*/; -%000001 state_t state_excl; + state_t state_incl /*verilator fsm_reset_arc*/; + state_t state_excl; -%000001 initial begin -%000001 rst = 1'b1; -%000001 cyc = 0; + always_ff @(posedge clk) begin + if (rst) state_incl <= S0; + else +%000001 case (state_incl) + // [FSM coverage] +%000001 // [fsm_arc t.reset_policy_u.state_incl::ANY->S0[reset_include]] [reset arc, excluded from %] +%000001 // [fsm_arc t.reset_policy_u.state_incl::S0->S1] +%000000 // [fsm_state t.reset_policy_u.state_incl::S0] *** UNCOVERED *** +%000001 // [fsm_state t.reset_policy_u.state_incl::S1] + S0: state_incl <= S1; + default: state_incl <= S1; + endcase end -%000006 always @(posedge clk) begin -%000006 cyc <= cyc + 1; -%000005 if (cyc == 1) rst <= 1'b0; -%000005 if (cyc == 5) begin -%000001 $write("*-* All Finished *-*\n"); -%000001 $finish; + always_ff @(posedge clk) begin + if (rst) state_excl <= S0; + else +%000001 case (state_excl) + // [FSM coverage] +%000001 // [fsm_arc t.reset_policy_u.state_excl::ANY->S0[reset]] [reset arc, excluded from %] +%000001 // [fsm_arc t.reset_policy_u.state_excl::S0->S1] +%000000 // [fsm_state t.reset_policy_u.state_excl::S0] *** UNCOVERED *** +%000001 // [fsm_state t.reset_policy_u.state_excl::S1] + S0: state_excl <= S1; + default: state_excl <= S1; + endcase + end + endmodule + + module fsm_reset_other_assign_ok ( + input logic clk, + input logic rst + ); + typedef enum logic [1:0] { + S0 = 2'b00, + S1 = 2'b01, + S2 = 2'b10 + } state_t; + + logic aux; + logic start; + state_t state_q; + state_t state_d; + + initial begin + aux = 1'b1; + start = 1'b1; + end + + always_ff @(posedge clk) begin + if (rst) begin + state_q <= S0; + aux <= 1'b0; + end else begin + state_q <= state_d; end end -%000006 always_ff @(posedge clk) begin -%000004 if (rst) state_incl <= S0; - else -%000004 case (state_incl) + always_comb begin + state_d = state_q; +%000002 case (state_q) // [FSM coverage] -%000001 // [fsm_arc t.state_incl::ANY->S0[reset_include]] [reset arc, excluded from %] -%000001 // [fsm_arc t.state_incl::S0->S1] -%000000 // [fsm_state t.state_incl::S0] *** UNCOVERED *** -%000001 // [fsm_state t.state_incl::S1] -%000001 S0: state_incl <= S1; -%000003 default: state_incl <= S1; - endcase +%000001 // [fsm_arc t.reset_other_assign_ok_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000002 // [fsm_arc t.reset_other_assign_ok_u.state_q::S0->S1] +%000000 // [fsm_arc t.reset_other_assign_ok_u.state_q::S0->S2] +%000002 // [fsm_state t.reset_other_assign_ok_u.state_q::S0] +%000002 // [fsm_state t.reset_other_assign_ok_u.state_q::S1] +%000000 // [fsm_state t.reset_other_assign_ok_u.state_q::S2] *** UNCOVERED *** + S0: state_d = start ? S1 : S2; + default: state_d = S0; + endcase + end + endmodule + + module fsm_oneblock_reset_mismatch_ok ( + input logic clk, + input logic rst + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t other_q; + + initial begin + state_q = S0; + other_q = S2; end -%000006 always_ff @(posedge clk) begin -%000004 if (rst) state_excl <= S0; - else -%000004 case (state_excl) + always_ff @(posedge clk) begin + if (rst) begin + other_q <= S0; + end else begin +%000001 case (state_q) // [FSM coverage] -%000001 // [fsm_arc t.state_excl::ANY->S0[reset]] [reset arc, excluded from %] -%000001 // [fsm_arc t.state_excl::S0->S1] -%000000 // [fsm_state t.state_excl::S0] *** UNCOVERED *** -%000001 // [fsm_state t.state_excl::S1] -%000001 S0: state_excl <= S1; -%000003 default: state_excl <= S1; +%000001 // [fsm_arc t.oneblock_reset_mismatch_ok_u.state_q::S0->S1] +%000000 // [fsm_state t.oneblock_reset_mismatch_ok_u.state_q::S0] *** UNCOVERED *** +%000001 // [fsm_state t.oneblock_reset_mismatch_ok_u.state_q::S1] +%000001 // [fsm_state t.oneblock_reset_mismatch_ok_u.state_q::S2] + S0: state_q <= S1; + default: state_q <= S2; endcase + end + end + endmodule + + module fsm_oneblock_reset_nonconst_ok ( + input logic clk, + input logic rst + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t other_q; + + initial begin + state_q = S0; + other_q = S2; + end + + // Useful because the one-block reset fallback must ignore non-constant reset + // assignments and still retain the supported case(state_q) FSM below. + always_ff @(posedge clk) begin + if (rst) begin + state_q <= other_q; + end else begin +%000001 case (state_q) + // [FSM coverage] +%000000 // [fsm_arc t.oneblock_reset_nonconst_ok_u.state_q::S0->S1] +%000000 // [fsm_state t.oneblock_reset_nonconst_ok_u.state_q::S0] *** UNCOVERED *** +%000000 // [fsm_state t.oneblock_reset_nonconst_ok_u.state_q::S1] *** UNCOVERED *** +%000001 // [fsm_state t.oneblock_reset_nonconst_ok_u.state_q::S2] + S0: state_q <= S1; + default: state_q <= S2; + endcase + end + end + endmodule + + module t ( + input logic clk + ); + + logic rst; + integer cyc; + + fsm_reset_policy reset_policy_u (.clk(clk), .rst(rst)); + fsm_reset_other_assign_ok reset_other_assign_ok_u (.clk(clk), .rst(rst)); + fsm_oneblock_reset_mismatch_ok oneblock_reset_mismatch_ok_u (.clk(clk), .rst(rst)); + fsm_oneblock_reset_nonconst_ok oneblock_reset_nonconst_ok_u (.clk(clk), .rst(rst)); + + initial begin + rst = 1'b1; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 5) begin + $write("*-* All Finished *-*\n"); + $finish; + end end endmodule diff --git a/test_regress/t/t_cover_fsm_reset.py b/test_regress/t/t_cover_fsm_reset.py index ee7db650a..13a780eaa 100755 --- a/test_regress/t/t_cover_fsm_reset.py +++ b/test_regress/t/t_cover_fsm_reset.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# DESCRIPTION: Verilator: FSM coverage reset policy test +# DESCRIPTION: Verilator: FSM coverage grouped reset semantics test # # This program is free software; you can redistribute it and/or modify it # under the terms of either the GNU Lesser General Public License Version 3 @@ -13,21 +13,10 @@ import vltest_bootstrap test.scenarios('simulator') -test.compile(verilator_flags2=['--cc --coverage']) +test.compile(verilator_flags2=['--cc --coverage-fsm']) test.execute() -test.run(cmd=[ - os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", - "--include-reset-arcs", - test.obj_dir + "/coverage.dat", -], - logfile=test.obj_dir + "/summary.log", - tee=False, - verilator_run=True) - -test.files_identical(test.obj_dir + "/summary.log", "t/" + test.name + "_summary.out") - test.run(cmd=[ os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", "--annotate", @@ -36,6 +25,17 @@ test.run(cmd=[ ], verilator_run=True) +test.run(cmd=[ + os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--include-reset-arcs", + "--annotate", + test.obj_dir + "/annotated-incl", + test.obj_dir + "/coverage.dat", +], + verilator_run=True) + test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) +test.files_identical(test.obj_dir + "/annotated-incl/" + test.name + ".v", + "t/" + test.name + "_incl.out") test.passes() diff --git a/test_regress/t/t_cover_fsm_reset.v b/test_regress/t/t_cover_fsm_reset.v index 4303fa771..5deaeb08a 100644 --- a/test_regress/t/t_cover_fsm_reset.v +++ b/test_regress/t/t_cover_fsm_reset.v @@ -1,11 +1,15 @@ -// DESCRIPTION: Verilator: FSM coverage reset policy test +// DESCRIPTION: Verilator: FSM coverage grouped reset semantics test // // This file ONLY is placed under the Creative Commons Public Domain. // SPDX-FileCopyrightText: 2026 Wilson Snyder // SPDX-License-Identifier: CC0-1.0 -module t ( - input clk +// Group accepted simulator-style reset semantics, including reset include / +// exclude behavior and nearby fallback cases that should still preserve FSMs. + +module fsm_reset_policy ( + input logic clk, + input logic rst ); typedef enum logic [0:0] { @@ -13,25 +17,9 @@ module t ( S1 = 1'b1 } state_t; - logic rst; - integer cyc; state_t state_incl /*verilator fsm_reset_arc*/; state_t state_excl; - initial begin - rst = 1'b1; - cyc = 0; - end - - always @(posedge clk) begin - cyc <= cyc + 1; - if (cyc == 1) rst <= 1'b0; - if (cyc == 5) begin - $write("*-* All Finished *-*\n"); - $finish; - end - end - always_ff @(posedge clk) begin if (rst) state_incl <= S0; else @@ -49,5 +37,132 @@ module t ( default: state_excl <= S1; endcase end +endmodule + +module fsm_reset_other_assign_ok ( + input logic clk, + input logic rst +); + typedef enum logic [1:0] { + S0 = 2'b00, + S1 = 2'b01, + S2 = 2'b10 + } state_t; + + logic aux; + logic start; + state_t state_q; + state_t state_d; + + initial begin + aux = 1'b1; + start = 1'b1; + end + + always_ff @(posedge clk) begin + if (rst) begin + state_q <= S0; + aux <= 1'b0; + end else begin + state_q <= state_d; + end + end + + always_comb begin + state_d = state_q; + case (state_q) + S0: state_d = start ? S1 : S2; + default: state_d = S0; + endcase + end +endmodule + +module fsm_oneblock_reset_mismatch_ok ( + input logic clk, + input logic rst +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t other_q; + + initial begin + state_q = S0; + other_q = S2; + end + + always_ff @(posedge clk) begin + if (rst) begin + other_q <= S0; + end else begin + case (state_q) + S0: state_q <= S1; + default: state_q <= S2; + endcase + end + end +endmodule + +module fsm_oneblock_reset_nonconst_ok ( + input logic clk, + input logic rst +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t other_q; + + initial begin + state_q = S0; + other_q = S2; + end + + // Useful because the one-block reset fallback must ignore non-constant reset + // assignments and still retain the supported case(state_q) FSM below. + always_ff @(posedge clk) begin + if (rst) begin + state_q <= other_q; + end else begin + case (state_q) + S0: state_q <= S1; + default: state_q <= S2; + endcase + end + end +endmodule + +module t ( + input logic clk +); + + logic rst; + integer cyc; + + fsm_reset_policy reset_policy_u (.clk(clk), .rst(rst)); + fsm_reset_other_assign_ok reset_other_assign_ok_u (.clk(clk), .rst(rst)); + fsm_oneblock_reset_mismatch_ok oneblock_reset_mismatch_ok_u (.clk(clk), .rst(rst)); + fsm_oneblock_reset_nonconst_ok oneblock_reset_nonconst_ok_u (.clk(clk), .rst(rst)); + + initial begin + rst = 1'b1; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 5) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end endmodule diff --git a/test_regress/t/t_cover_fsm_reset_incl.out b/test_regress/t/t_cover_fsm_reset_incl.out new file mode 100644 index 000000000..7f79dfb75 --- /dev/null +++ b/test_regress/t/t_cover_fsm_reset_incl.out @@ -0,0 +1,197 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM coverage grouped reset semantics test + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + // Group accepted simulator-style reset semantics, including reset include / + // exclude behavior and nearby fallback cases that should still preserve FSMs. + + module fsm_reset_policy ( + input logic clk, + input logic rst + ); + + typedef enum logic [0:0] { + S0 = 1'b0, + S1 = 1'b1 + } state_t; + + state_t state_incl /*verilator fsm_reset_arc*/; + state_t state_excl; + + always_ff @(posedge clk) begin + if (rst) state_incl <= S0; + else +%000001 case (state_incl) + // [FSM coverage] +%000001 // [fsm_arc t.reset_policy_u.state_incl::ANY->S0[reset_include]] +%000001 // [fsm_arc t.reset_policy_u.state_incl::S0->S1] +%000000 // [fsm_state t.reset_policy_u.state_incl::S0] *** UNCOVERED *** +%000001 // [fsm_state t.reset_policy_u.state_incl::S1] + S0: state_incl <= S1; + default: state_incl <= S1; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_excl <= S0; + else +%000001 case (state_excl) + // [FSM coverage] +%000001 // [fsm_arc t.reset_policy_u.state_excl::ANY->S0[reset]] +%000001 // [fsm_arc t.reset_policy_u.state_excl::S0->S1] +%000000 // [fsm_state t.reset_policy_u.state_excl::S0] *** UNCOVERED *** +%000001 // [fsm_state t.reset_policy_u.state_excl::S1] + S0: state_excl <= S1; + default: state_excl <= S1; + endcase + end + endmodule + + module fsm_reset_other_assign_ok ( + input logic clk, + input logic rst + ); + typedef enum logic [1:0] { + S0 = 2'b00, + S1 = 2'b01, + S2 = 2'b10 + } state_t; + + logic aux; + logic start; + state_t state_q; + state_t state_d; + + initial begin + aux = 1'b1; + start = 1'b1; + end + + always_ff @(posedge clk) begin + if (rst) begin + state_q <= S0; + aux <= 1'b0; + end else begin + state_q <= state_d; + end + end + + always_comb begin + state_d = state_q; +%000002 case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.reset_other_assign_ok_u.state_q::ANY->S0[reset]] +%000002 // [fsm_arc t.reset_other_assign_ok_u.state_q::S0->S1] +%000000 // [fsm_arc t.reset_other_assign_ok_u.state_q::S0->S2] +%000002 // [fsm_state t.reset_other_assign_ok_u.state_q::S0] +%000002 // [fsm_state t.reset_other_assign_ok_u.state_q::S1] +%000000 // [fsm_state t.reset_other_assign_ok_u.state_q::S2] *** UNCOVERED *** + S0: state_d = start ? S1 : S2; + default: state_d = S0; + endcase + end + endmodule + + module fsm_oneblock_reset_mismatch_ok ( + input logic clk, + input logic rst + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t other_q; + + initial begin + state_q = S0; + other_q = S2; + end + + always_ff @(posedge clk) begin + if (rst) begin + other_q <= S0; + end else begin +%000001 case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.oneblock_reset_mismatch_ok_u.state_q::S0->S1] +%000000 // [fsm_state t.oneblock_reset_mismatch_ok_u.state_q::S0] *** UNCOVERED *** +%000001 // [fsm_state t.oneblock_reset_mismatch_ok_u.state_q::S1] +%000001 // [fsm_state t.oneblock_reset_mismatch_ok_u.state_q::S2] + S0: state_q <= S1; + default: state_q <= S2; + endcase + end + end + endmodule + + module fsm_oneblock_reset_nonconst_ok ( + input logic clk, + input logic rst + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t other_q; + + initial begin + state_q = S0; + other_q = S2; + end + + // Useful because the one-block reset fallback must ignore non-constant reset + // assignments and still retain the supported case(state_q) FSM below. + always_ff @(posedge clk) begin + if (rst) begin + state_q <= other_q; + end else begin +%000001 case (state_q) + // [FSM coverage] +%000000 // [fsm_arc t.oneblock_reset_nonconst_ok_u.state_q::S0->S1] +%000000 // [fsm_state t.oneblock_reset_nonconst_ok_u.state_q::S0] *** UNCOVERED *** +%000000 // [fsm_state t.oneblock_reset_nonconst_ok_u.state_q::S1] *** UNCOVERED *** +%000001 // [fsm_state t.oneblock_reset_nonconst_ok_u.state_q::S2] + S0: state_q <= S1; + default: state_q <= S2; + endcase + end + end + endmodule + + module t ( + input logic clk + ); + + logic rst; + integer cyc; + + fsm_reset_policy reset_policy_u (.clk(clk), .rst(rst)); + fsm_reset_other_assign_ok reset_other_assign_ok_u (.clk(clk), .rst(rst)); + fsm_oneblock_reset_mismatch_ok oneblock_reset_mismatch_ok_u (.clk(clk), .rst(rst)); + fsm_oneblock_reset_nonconst_ok oneblock_reset_nonconst_ok_u (.clk(clk), .rst(rst)); + + initial begin + rst = 1'b1; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 5) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + endmodule + diff --git a/test_regress/t/t_cover_fsm_reset_multi.out b/test_regress/t/t_cover_fsm_reset_multi.out index c74be0d53..dd19f5207 100644 --- a/test_regress/t/t_cover_fsm_reset_multi.out +++ b/test_regress/t/t_cover_fsm_reset_multi.out @@ -1,63 +1,6 @@ -// // verilator_coverage annotation - // DESCRIPTION: Verilator: FSM coverage reset pseudo-vertex reuse test - // - // This file ONLY is placed under the Creative Commons Public Domain. - // SPDX-FileCopyrightText: 2026 Wilson Snyder - // SPDX-License-Identifier: CC0-1.0 - - module t ( - input clk - ); - - typedef enum logic [1:0] { - S0 = 2'd0, - S1 = 2'd1, - S2 = 2'd2 - } state_t; - - logic rst; - integer cyc; - state_t state /*verilator fsm_reset_arc*/; - - initial begin - rst = 1'b1; - cyc = 0; - end - - always @(posedge clk) begin - cyc <= cyc + 1; - if (cyc == 1) rst <= 1'b0; - if (cyc == 5) begin - $write("*-* All Finished *-*\n"); - $finish; - end - end - - // This reset block is intentionally non-idiomatic. The detector only collects - // reset arcs from top-level direct assignments in the reset branch, so two - // sequential assignments are the narrowest way to force multiple reset arcs - // into one FSM graph and exercise reuse of the synthetic ANY reset source. - always_ff @(posedge clk) begin - if (rst) begin - state <= S0; - state <= S1; - end - else begin -%000001 case (state) - // [FSM coverage] -%000000 // [fsm_arc t.state::ANY->S0[reset_include]] [reset arc, excluded from %] -%000001 // [fsm_arc t.state::ANY->S1[reset_include]] [reset arc, excluded from %] -%000000 // [fsm_arc t.state::S0->S2] -%000001 // [fsm_arc t.state::S1->S2] -%000000 // [fsm_state t.state::S0] *** UNCOVERED *** -%000001 // [fsm_state t.state::S1] -%000001 // [fsm_state t.state::S2] - S0: state <= S2; - S1: state <= S2; - default: state <= S2; - endcase - end - end - - endmodule - +%Warning-COVERIGN: t/t_cover_fsm_reset_multi.v:41:13: Ignoring unsupported: FSM coverage on reset branches with multiple assignments to the state variable + 41 | state <= S1; + | ^~ + ... For warning description see https://verilator.org/warn/COVERIGN?v=latest + ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. +%Error: Exiting due to diff --git a/test_regress/t/t_cover_fsm_reset_multi.py b/test_regress/t/t_cover_fsm_reset_multi.py index 42ab4d0f3..6689f3ab2 100755 --- a/test_regress/t/t_cover_fsm_reset_multi.py +++ b/test_regress/t/t_cover_fsm_reset_multi.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# DESCRIPTION: Verilator: FSM coverage reset pseudo-vertex reuse test +# DESCRIPTION: Verilator: FSM coverage multi-reset assignment warning test # # This program is free software; you can redistribute it and/or modify it # under the terms of either the GNU Lesser General Public License Version 3 @@ -7,30 +7,14 @@ # SPDX-FileCopyrightText: 2026 Wilson Snyder # SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 -import os - import vltest_bootstrap -test.scenarios('simulator') +test.scenarios('vlt') -# This regression is aimed at the graph helper, not at recommending RTL style. -# We deliberately create two reset arcs in a single FSM so graph construction -# has to reuse the synthetic ANY reset pseudo-vertex rather than allocating it -# only once for a one-arc machine. -test.compile(verilator_flags2=['--cc --coverage-fsm']) - -test.execute() - -# Use annotated-source output so the golden proves both reset arcs remain -# visible and share the same synthetic ANY reset source. -test.run(cmd=[ - os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", - "--annotate", - test.obj_dir + "/annotated", - test.obj_dir + "/coverage.dat", -], - verilator_run=True) - -test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) +# Multiple reset assignments to the same FSM state variable are legal RTL but +# not a realistic reset style to model as distinct reset arcs. Warn and ignore +# reset-arc extraction for that branch instead of inventing multiple ANY->state +# coverage edges. +test.lint(verilator_flags2=["--coverage-fsm"], fails=True, expect_filename=test.golden_filename) test.passes() diff --git a/test_regress/t/t_cover_fsm_reset_multi.v b/test_regress/t/t_cover_fsm_reset_multi.v index 31dc4e748..bf7e87bf7 100644 --- a/test_regress/t/t_cover_fsm_reset_multi.v +++ b/test_regress/t/t_cover_fsm_reset_multi.v @@ -1,4 +1,4 @@ -// DESCRIPTION: Verilator: FSM coverage reset pseudo-vertex reuse test +// DESCRIPTION: Verilator: FSM coverage multi-reset assignment warning test // // This file ONLY is placed under the Creative Commons Public Domain. // SPDX-FileCopyrightText: 2026 Wilson Snyder @@ -32,10 +32,9 @@ module t ( end end - // This reset block is intentionally non-idiomatic. The detector only collects - // reset arcs from top-level direct assignments in the reset branch, so two - // sequential assignments are the narrowest way to force multiple reset arcs - // into one FSM graph and exercise reuse of the synthetic ANY reset source. + // This reset block is intentionally non-idiomatic. Multiple assignments to + // the FSM state variable in the reset branch should warn and be ignored for + // reset-arc extraction rather than inventing multiple reset transitions. always_ff @(posedge clk) begin if (rst) begin state <= S0; diff --git a/test_regress/t/t_cover_fsm_reset_summary.out b/test_regress/t/t_cover_fsm_reset_summary.out deleted file mode 100644 index 32439a8ea..000000000 --- a/test_regress/t/t_cover_fsm_reset_summary.out +++ /dev/null @@ -1,7 +0,0 @@ -Coverage Summary: - line : 100.0% (8/8) - toggle : 75.0% (6/8) - branch : 100.0% (8/8) - expr : 0.0% (0/0) - fsm_state : 50.0% (2/4) - fsm_arc : 100.0% (4/4) diff --git a/test_regress/t/t_cover_fsm_styles.out b/test_regress/t/t_cover_fsm_styles.out deleted file mode 100644 index ac3171825..000000000 --- a/test_regress/t/t_cover_fsm_styles.out +++ /dev/null @@ -1,67 +0,0 @@ -// // verilator_coverage annotation - // DESCRIPTION: Verilator: FSM coverage style coverage test - // - // This file ONLY is placed under the Creative Commons Public Domain. - // SPDX-FileCopyrightText: 2026 Wilson Snyder - // SPDX-License-Identifier: CC0-1.0 - - module t ( - input clk - ); - - typedef enum logic [1:0] { - S0 = 2'd0, - S1 = 2'd1, - S2 = 2'd2, - S3 = 2'd3 - } state_t; - - integer cyc; - logic rst; - logic start; - state_t state /*verilator fsm_arc_include_cond*/; - - initial begin - rst = 1'b1; - start = 1'b0; - cyc = 0; - end - - always @(posedge clk) begin - cyc <= cyc + 1; - if (cyc == 1) rst <= 1'b0; - if (cyc == 2) start <= 1'b1; - if (cyc == 3) start <= 1'b0; - if (cyc == 6) begin - $write("*-* All Finished *-*\n"); - $finish; - end - end - - always_ff @(posedge clk) begin - if (rst) begin - state <= S0; - end - else begin -%000003 case (state) - // [FSM coverage] -%000001 // [fsm_arc t.state::ANY->S0[reset]] [reset arc, excluded from %] -%000000 // [fsm_arc t.state::S0->S1] -%000003 // [fsm_arc t.state::S0->S2] -%000000 // [fsm_arc t.state::S1->S3] -%000000 // [SYNTHETIC DEFAULT ARC: t.state::default->S0] -%000002 // [fsm_state t.state::S0] -%000000 // [fsm_state t.state::S1] *** UNCOVERED *** -%000003 // [fsm_state t.state::S2] -%000000 // [fsm_state t.state::S3] *** UNCOVERED *** - S0: - if (start) state <= S1; - else state <= S2; - S1: state <= S3; - default: state <= S0; - endcase - end - end - - endmodule - diff --git a/test_regress/t/t_cover_fsm_styles.v b/test_regress/t/t_cover_fsm_styles.v deleted file mode 100644 index a07022b10..000000000 --- a/test_regress/t/t_cover_fsm_styles.v +++ /dev/null @@ -1,55 +0,0 @@ -// DESCRIPTION: Verilator: FSM coverage style coverage test -// -// This file ONLY is placed under the Creative Commons Public Domain. -// SPDX-FileCopyrightText: 2026 Wilson Snyder -// SPDX-License-Identifier: CC0-1.0 - -module t ( - input clk -); - - typedef enum logic [1:0] { - S0 = 2'd0, - S1 = 2'd1, - S2 = 2'd2, - S3 = 2'd3 - } state_t; - - integer cyc; - logic rst; - logic start; - state_t state /*verilator fsm_arc_include_cond*/; - - initial begin - rst = 1'b1; - start = 1'b0; - cyc = 0; - end - - always @(posedge clk) begin - cyc <= cyc + 1; - if (cyc == 1) rst <= 1'b0; - if (cyc == 2) start <= 1'b1; - if (cyc == 3) start <= 1'b0; - if (cyc == 6) begin - $write("*-* All Finished *-*\n"); - $finish; - end - end - - always_ff @(posedge clk) begin - if (rst) begin - state <= S0; - end - else begin - case (state) - S0: - if (start) state <= S1; - else state <= S2; - S1: state <= S3; - default: state <= S0; - endcase - end - end - -endmodule diff --git a/test_regress/t/t_cover_fsm_transition_shapes_multi.out b/test_regress/t/t_cover_fsm_transition_shapes_multi.out new file mode 100644 index 000000000..8b8f255e5 --- /dev/null +++ b/test_regress/t/t_cover_fsm_transition_shapes_multi.out @@ -0,0 +1 @@ +# SystemC::Coverage-3 diff --git a/test_regress/t/t_cover_fsm_styles.py b/test_regress/t/t_cover_fsm_transition_shapes_multi.py similarity index 54% rename from test_regress/t/t_cover_fsm_styles.py rename to test_regress/t/t_cover_fsm_transition_shapes_multi.py index fc9fde9f0..46c209f35 100755 --- a/test_regress/t/t_cover_fsm_styles.py +++ b/test_regress/t/t_cover_fsm_transition_shapes_multi.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# DESCRIPTION: Verilator: FSM coverage style coverage test +# DESCRIPTION: Verilator: FSM coverage ignores grouped unsupported FSM extraction patterns # # This program is free software; you can redistribute it and/or modify it # under the terms of either the GNU Lesser General Public License Version 3 @@ -7,8 +7,6 @@ # SPDX-FileCopyrightText: 2026 Wilson Snyder # SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 -import os - import vltest_bootstrap test.scenarios('simulator') @@ -17,11 +15,6 @@ test.compile(verilator_flags2=['--cc --coverage-fsm']) test.execute() -test.run(cmd=[os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", - "--annotate", test.obj_dir + "/annotated", - test.obj_dir + "/coverage.dat"], - verilator_run=True) # yapf:disable - -test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) +test.files_identical(test.obj_dir + "/coverage.dat", "t/" + test.name + ".out") test.passes() diff --git a/test_regress/t/t_cover_fsm_transition_shapes_multi.v b/test_regress/t/t_cover_fsm_transition_shapes_multi.v new file mode 100644 index 000000000..ae7103724 --- /dev/null +++ b/test_regress/t/t_cover_fsm_transition_shapes_multi.v @@ -0,0 +1,819 @@ +// DESCRIPTION: Verilator: FSM coverage ignores grouped unsupported FSM extraction patterns +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// Group unsupported extraction shapes that must compile and run cleanly while +// emitting no FSM coverage points at all. + +module fsm_caseitem_varrhs_bad ( + input logic clk +); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + logic start; + integer cyc; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + state_t other_d; + + initial begin + start = 1'b0; + cyc = 0; + other_d = S1; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + end + + always_comb begin + state_d = state_q; + case (state_q) + S0: state_d = other_d; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + state_q <= state_d; + end +endmodule + +module fsm_caseitem_then_nonconst_bad ( + input logic clk +); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + + logic sel; + integer cyc; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + state_t other_d; + + initial begin + sel = 1'b0; + cyc = 0; + other_d = S1; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 2) sel <= 1'b1; + if (cyc == 3) sel <= 1'b0; + end + + always_comb begin + state_d = state_q; + /* verilator lint_off CASEINCOMPLETE */ + case (state_q) + S0: state_d = sel ? other_d : S1; + endcase + /* verilator lint_on CASEINCOMPLETE */ + end + + always_ff @(posedge clk) begin + state_q <= state_d; + end +endmodule + +module fsm_caseitem_else_nonconst_bad ( + input logic clk +); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + + logic sel; + integer cyc; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + state_t other_d; + + initial begin + sel = 1'b0; + cyc = 0; + other_d = S1; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 2) sel <= 1'b1; + if (cyc == 3) sel <= 1'b0; + end + + always_comb begin + state_d = state_q; + /* verilator lint_off CASEINCOMPLETE */ + case (state_q) + S0: state_d = sel ? S1 : other_d; + endcase + /* verilator lint_on CASEINCOMPLETE */ + end + + always_ff @(posedge clk) begin + state_q <= state_d; + end +endmodule + +module fsm_oneblock_then_bad ( + input logic clk +); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + logic rst; + logic sel; + integer cyc; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + + initial begin + rst = 1'b1; + sel = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) sel <= 1'b1; + if (cyc == 3) sel <= 1'b0; + end + + always_comb begin + state_d = state_q; + case (state_q) + S0: state_d = sel ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + state_q <= rst ? (sel ? S0 : S1) : state_d; + end +endmodule + +module fsm_oneblock_else_bad ( + input logic clk +); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + logic rst; + logic sel; + integer cyc; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + + initial begin + rst = 1'b1; + sel = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) sel <= 1'b1; + if (cyc == 3) sel <= 1'b0; + end + + always_comb begin + state_d = state_q; + case (state_q) + S0: state_d = sel ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + state_q <= rst ? S0 : (sel ? S1 : state_d); + end +endmodule + +module fsm_combo_sel_expr_bad ( + input logic clk +); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + logic rst; + logic start; + integer cyc; + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + + initial begin + rst = 1'b1; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + end + + always_comb begin + state_d = state_q; + case (state_q + 1) + S0: state_d = start ? S1 : S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) begin + state_q <= S0; + end else begin + state_q <= state_d; + end + end +endmodule + +module fsm_normalized_if_noelse_bad ( + input logic clk +); + typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t; + logic rst; + logic start; + integer cyc; + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + + initial begin + rst = 1'b1; + start = 1'b0; + cyc = 0; + end + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + end + always_comb begin + state_d = state_q; + case (state_q) + S0: if (start) ; else state_d = S1; + default: state_d = S0; + endcase + end + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule + +module fsm_normalized_if_then_nonconst_bad ( + input logic clk +); + typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t; + logic sel; + integer cyc; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + state_t tmp_a; + state_t tmp_b; + state_t other_d; + + initial begin + sel = 1'b0; + cyc = 0; + other_d = S2; + end + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 2) sel <= 1'b1; + if (cyc == 3) sel <= 1'b0; + end + always_comb begin + state_d = state_q; + case (state_q) + S0: begin + if (sel) tmp_a = other_d; + else tmp_b = S1; + state_d = tmp_a; + end + default: state_d = S0; + endcase + end + always_ff @(posedge clk) begin + state_q <= state_d; + end +endmodule + +module fsm_normalized_if_else_nonconst_bad ( + input logic clk +); + typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t; + logic sel; + integer cyc; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + state_t tmp_a; + state_t tmp_b; + state_t other_d; + + initial begin + sel = 1'b0; + cyc = 0; + other_d = S2; + end + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 2) sel <= 1'b1; + if (cyc == 3) sel <= 1'b0; + end + always_comb begin + state_d = state_q; + case (state_q) + S0: begin + if (sel) tmp_a = S1; + else tmp_b = other_d; + state_d = tmp_a; + end + default: state_d = S0; + endcase + end + always_ff @(posedge clk) begin + state_q <= state_d; + end +endmodule + +module fsm_normalized_if_temp_mismatch_bad ( + input logic clk +); + typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t; + logic sel; + integer cyc; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + state_t tmp_a; + state_t tmp_b; + + initial begin + sel = 1'b0; + cyc = 0; + end + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 2) sel <= 1'b1; + if (cyc == 3) sel <= 1'b0; + end + always_comb begin + state_d = state_q; + case (state_q) + S0: begin + if (sel) tmp_a = S1; + else tmp_b = S2; + state_d = tmp_a; + end + default: state_d = S0; + endcase + end + always_ff @(posedge clk) begin + state_q <= state_d; + end +endmodule + +module fsm_normalized_if_then_state_else_other_bad ( + input logic clk +); + typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t; + logic sel; + integer cyc; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + state_t tmp_a; + + initial begin + sel = 1'b0; + cyc = 0; + end + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 2) sel <= 1'b1; + if (cyc == 3) sel <= 1'b0; + end + always_comb begin + state_d = state_q; + case (state_q) + S0: begin + if (sel) state_d = S1; + else tmp_a = S2; + state_d = tmp_a; + end + default: state_d = S0; + endcase + end + always_ff @(posedge clk) begin + state_q <= state_d; + end +endmodule + +module fsm_normalized_if_same_temp_nofollow_bad ( + input logic clk +); + typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t; + logic sel; + logic aux; + integer cyc; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + state_t tmp_a; + + initial begin + sel = 1'b0; + cyc = 0; + end + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 2) sel <= 1'b1; + if (cyc == 3) sel <= 1'b0; + end + always_comb begin + state_d = state_q; + case (state_q) + S0: begin + if (sel) begin + tmp_a = S1; + aux = 1'b1; + end else begin + tmp_a = S2; + aux = 1'b0; + end + end + default: state_d = S0; + endcase + end + always_ff @(posedge clk) begin + state_q <= state_d; + end +endmodule + +module fsm_normalized_if_follow_nonvar_bad ( + input logic clk +); + typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t; + logic sel; + logic aux; + integer cyc; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + state_t tmp_a; + + initial begin + sel = 1'b0; + cyc = 0; + end + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 2) sel <= 1'b1; + if (cyc == 3) sel <= 1'b0; + end + always_comb begin + state_d = state_q; + case (state_q) + S0: begin + if (sel) begin + tmp_a = S1; + aux = 1'b1; + end else begin + tmp_a = S2; + aux = 1'b0; + end + // Final follow-up is not a plain var-to-var state assignment. + aux = (tmp_a == S1); + end + default: state_d = S0; + endcase + end + always_ff @(posedge clk) begin + state_q <= state_d; + end +endmodule + +module fsm_normalized_if_follow_wrongfrom_bad ( + input logic clk +); + typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t; + logic sel; + logic aux; + integer cyc; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + state_t tmp_a; + state_t other_d; + + initial begin + sel = 1'b0; + cyc = 0; + end + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 2) sel <= 1'b1; + if (cyc == 3) sel <= 1'b0; + end + always_comb begin + state_d = state_q; + case (state_q) + S0: begin + if (sel) begin + tmp_a = S1; + aux = 1'b1; + end else begin + tmp_a = S2; + aux = 1'b0; + end + // Final follow-up reads from the wrong source temp. + state_d = other_d; + end + default: state_d = S0; + endcase + end + always_ff @(posedge clk) begin + state_q <= state_d; + end +endmodule + +module fsm_normalized_if_follow_wronglhs_bad ( + input logic clk +); + typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t; + logic sel; + logic aux; + integer cyc; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + state_t tmp_a; + state_t other_d; + + initial begin + sel = 1'b0; + cyc = 0; + end + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 2) sel <= 1'b1; + if (cyc == 3) sel <= 1'b0; + end + always_comb begin + state_d = state_q; + case (state_q) + S0: begin + if (sel) begin + tmp_a = S1; + aux = 1'b1; + end else begin + tmp_a = S2; + aux = 1'b0; + end + // Final follow-up writes the wrong lhs instead of state_d. + other_d = tmp_a; + end + default: state_d = S0; + endcase + end + always_ff @(posedge clk) begin + state_q <= state_d; + end +endmodule + +module fsm_case_next_wrongrhs_bad ( + input logic clk +); + typedef enum logic [1:0] { S0 = 2'd0, S1 = 2'd1, S2 = 2'd2 } state_t; + logic rst; + logic start; + integer cyc; + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + state_t other_d; + + initial begin + rst = 1'b1; + start = 1'b0; + cyc = 0; + other_d = S1; + end + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + end + always_comb begin + state_d = other_d; + case (state_d) + S0: state_d = start ? S1 : S2; + default: state_d = S0; + endcase + end + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule + +module fsm_forced_wide_bad ( + input logic clk +); + + integer cyc; + logic rst; + logic [30:0] state /*verilator fsm_state*/; + + initial begin + cyc = 0; + rst = 1'b1; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 6) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + always_ff @(posedge clk) begin + if (rst) begin + state <= 31'd0; + end else begin + case (state) + 31'd0: state <= 31'd1; + 31'd1: state <= 31'd2; + default: state <= 31'd0; + endcase + end + end + +endmodule + +module fsm_reset_commit_mismatch_bad ( + input logic clk +); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + logic rst; + logic start; + integer cyc; + state_t state_q /*verilator fsm_state*/; + state_t other_q; + state_t state_d; + + initial begin + rst = 1'b1; + start = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + always_comb begin + state_d = state_q; + case (state_q) + S0: state_d = start ? S1 : S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) other_q <= S0; + else state_q <= state_d; + end + +endmodule + +module fsm_reset_then_bad ( + input logic clk +); + + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + logic rst; + logic sel; + integer cyc; + state_t state_q /*verilator fsm_state*/; + state_t state_d; + + initial begin + rst = 1'b1; + sel = 1'b0; + cyc = 0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) sel <= 1'b1; + if (cyc == 3) sel <= 1'b0; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end + + always_comb begin + state_d = state_q; + case (state_q) + S0: state_d = sel ? S1 : S0; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= sel ? S0 : S1; + else state_q <= state_d; + end + +endmodule + +module t ( + input logic clk +); + + integer cyc; + + fsm_caseitem_varrhs_bad caseitem_varrhs_bad_u (.clk(clk)); + fsm_caseitem_then_nonconst_bad caseitem_then_nonconst_bad_u (.clk(clk)); + fsm_caseitem_else_nonconst_bad caseitem_else_nonconst_bad_u (.clk(clk)); + fsm_oneblock_then_bad oneblock_then_bad_u (.clk(clk)); + fsm_oneblock_else_bad oneblock_else_bad_u (.clk(clk)); + fsm_combo_sel_expr_bad combo_sel_expr_bad_u (.clk(clk)); + fsm_normalized_if_noelse_bad normalized_if_noelse_bad_u (.clk(clk)); + fsm_normalized_if_then_nonconst_bad normalized_if_then_nonconst_bad_u (.clk(clk)); + fsm_normalized_if_else_nonconst_bad normalized_if_else_nonconst_bad_u (.clk(clk)); + fsm_normalized_if_temp_mismatch_bad normalized_if_temp_mismatch_bad_u (.clk(clk)); + fsm_normalized_if_then_state_else_other_bad normalized_if_then_state_else_other_bad_u (.clk(clk)); + fsm_normalized_if_same_temp_nofollow_bad normalized_if_same_temp_nofollow_bad_u (.clk(clk)); + fsm_normalized_if_follow_nonvar_bad normalized_if_follow_nonvar_bad_u (.clk(clk)); + fsm_normalized_if_follow_wrongfrom_bad normalized_if_follow_wrongfrom_bad_u (.clk(clk)); + fsm_normalized_if_follow_wronglhs_bad normalized_if_follow_wronglhs_bad_u (.clk(clk)); + fsm_case_next_wrongrhs_bad case_next_wrongrhs_bad_u (.clk(clk)); + fsm_forced_wide_bad forced_wide_bad_u (.clk(clk)); + fsm_reset_commit_mismatch_bad reset_commit_mismatch_bad_u (.clk(clk)); + fsm_reset_then_bad reset_then_bad_u (.clk(clk)); + + initial cyc = 0; + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end +endmodule diff --git a/test_regress/t/t_cover_fsm_two_proc_multi.out b/test_regress/t/t_cover_fsm_two_proc_multi.out new file mode 100644 index 000000000..42e8edefd --- /dev/null +++ b/test_regress/t/t_cover_fsm_two_proc_multi.out @@ -0,0 +1,459 @@ +// // verilator_coverage annotation + // DESCRIPTION: Verilator: FSM coverage combined two-process/three-block regression + // + // This file ONLY is placed under the Creative Commons Public Domain. + // SPDX-FileCopyrightText: 2026 Wilson Snyder + // SPDX-License-Identifier: CC0-1.0 + + // Group supported multi-process and nearby supported-core variants so the + // main two-process / three-block extraction family stays in one place. + + module fsm_basic ( +%000009 input logic clk, +%000001 input logic rst, +%000001 input logic start + ); + typedef enum logic [1:0] { + S_IDLE = 2'd0, + S_RUN = 2'd1, + S_DONE = 2'd2, + S_ERR = 2'd3 + } state_t; + +%000002 state_t state_q /*verilator fsm_reset_arc*/; +%000002 state_t state_d; + + 000010 always_comb begin + 000010 state_d = state_q; +~000010 case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.basic_u.state_q::ANY->S_IDLE[reset_include]] [reset arc, excluded from %] +%000001 // [fsm_arc t.basic_u.state_q::S_RUN->S_DONE] +%000001 // [fsm_state t.basic_u.state_q::S_DONE] +%000001 // [fsm_state t.basic_u.state_q::S_ERR] +%000000 // [fsm_state t.basic_u.state_q::S_IDLE] *** UNCOVERED *** +%000001 // [fsm_state t.basic_u.state_q::S_RUN] +%000004 S_IDLE: if (start) state_d = S_RUN; +%000001 S_RUN: state_d = S_DONE; +%000005 default: state_d = S_ERR; + endcase + end + +%000009 always_ff @(posedge clk) begin +%000007 if (rst) state_q <= S_IDLE; +%000007 else state_q <= state_d; + end + endmodule + + module fsm_three_block ( +%000009 input logic clk, +%000001 input logic rst, +%000001 input logic start + ); + typedef enum logic [1:0] { + S_IDLE = 2'd0, + S_BUSY = 2'd1, + S_DONE = 2'd2 + } state_t; + +%000001 logic busy; +%000001 logic done; +%000001 state_t state_q; +%000001 state_t state_d; + + 000010 always_comb begin + 000010 state_d = state_q; +~000010 case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.three_block_u.state_q::ANY->S_IDLE[reset]] [reset arc, excluded from %] +%000001 // [fsm_arc t.three_block_u.state_q::S_BUSY->S_DONE] +%000001 // [fsm_arc t.three_block_u.state_q::S_DONE->S_IDLE] +%000001 // [fsm_state t.three_block_u.state_q::S_BUSY] +%000001 // [fsm_state t.three_block_u.state_q::S_DONE] +%000001 // [fsm_state t.three_block_u.state_q::S_IDLE] +%000008 S_IDLE: if (start) state_d = S_BUSY; +%000001 S_BUSY: state_d = S_DONE; +%000001 S_DONE: state_d = S_IDLE; +%000000 default: state_d = S_IDLE; + endcase + end + +%000001 always_comb begin +%000001 busy = (state_q == S_BUSY); +%000001 done = (state_q == S_DONE); + end + +%000009 always_ff @(posedge clk) begin +%000007 if (rst) state_q <= S_IDLE; +%000007 else state_q <= state_d; + end + endmodule + + module fsm_mealy ( +%000009 input logic clk, +%000001 input logic rst, +%000001 input logic start, +%000001 input logic bit_done + ); + typedef enum logic [1:0] { + S_IDLE = 2'd0, + S_SEND = 2'd1, + S_DONE = 2'd2 + } state_t; + +%000002 logic tx; +%000001 state_t state_q; +%000001 state_t state_d; + + 000010 always_comb begin + 000010 state_d = state_q; + 000010 tx = 1'b1; +~000010 case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.mealy_u.state_q::ANY->S_IDLE[reset]] [reset arc, excluded from %] +%000001 // [fsm_arc t.mealy_u.state_q::S_DONE->S_IDLE] +%000001 // [fsm_state t.mealy_u.state_q::S_DONE] +%000001 // [fsm_state t.mealy_u.state_q::S_IDLE] +%000001 // [fsm_state t.mealy_u.state_q::S_SEND] +%000007 S_IDLE: begin +%000006 if (start) begin +%000001 state_d = S_SEND; +%000001 tx = 1'b0; + end + end +%000002 S_SEND: begin +%000002 tx = bit_done; +%000001 if (bit_done) state_d = S_DONE; + end +%000001 S_DONE: state_d = S_IDLE; +%000000 default: state_d = S_IDLE; + endcase + end + +%000009 always_ff @(posedge clk) begin +%000007 if (rst) state_q <= S_IDLE; +%000007 else state_q <= state_d; + end + endmodule + + module fsm_reset_policy ( +%000009 input logic clk, +%000001 input logic rst + ); + typedef enum logic [0:0] { + S0 = 1'b0, + S1 = 1'b1 + } state_t; + +%000001 state_t state_incl_q /*verilator fsm_reset_arc*/; +%000001 state_t state_incl_d; +%000001 state_t state_excl_q; +%000001 state_t state_excl_d; + + 000010 always_comb begin + 000010 state_incl_d = state_incl_q; +~000010 case (state_incl_q) + // [FSM coverage] +%000001 // [fsm_arc t.reset_u.state_incl_q::ANY->S0[reset_include]] [reset arc, excluded from %] +%000001 // [fsm_arc t.reset_u.state_incl_q::S0->S1] +%000000 // [fsm_state t.reset_u.state_incl_q::S0] *** UNCOVERED *** +%000001 // [fsm_state t.reset_u.state_incl_q::S1] +%000003 S0: state_incl_d = S1; +%000007 default: state_incl_d = S1; + endcase + end + + 000010 always_comb begin + 000010 state_excl_d = state_excl_q; +~000010 case (state_excl_q) + // [FSM coverage] +%000001 // [fsm_arc t.reset_u.state_excl_q::ANY->S0[reset]] [reset arc, excluded from %] +%000001 // [fsm_arc t.reset_u.state_excl_q::S0->S1] +%000000 // [fsm_state t.reset_u.state_excl_q::S0] *** UNCOVERED *** +%000001 // [fsm_state t.reset_u.state_excl_q::S1] +%000003 S0: state_excl_d = S1; +%000007 default: state_excl_d = S1; + endcase + end + +%000009 always_ff @(posedge clk) begin +%000007 if (rst) state_incl_q <= S0; +%000007 else state_incl_q <= state_incl_d; + end + +%000009 always_ff @(posedge clk) begin +%000007 if (rst) state_excl_q <= S0; +%000007 else state_excl_q <= state_excl_d; + end + endmodule + + module fsm_nextstate_sel_off ( +%000009 input logic clk + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + +%000001 state_t state_q; +%000001 state_t state_d; + +%000001 always_comb begin +%000001 state_d = S0; +%000001 case (state_d) +%000001 S0: state_d = S1; +%000000 default: state_d = S0; + endcase + end + +%000009 always_ff @(posedge clk) begin +%000009 state_q <= state_d; + end + endmodule + + module fsm_nextstate_sel_ok ( +%000009 input logic clk, +%000001 input logic rst, +%000001 input logic start + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + +%000001 state_t state_q; +%000001 state_t state_d; + + 000010 always_comb begin + 000010 state_d = state_q; +~000010 case (state_d) + // [FSM coverage] +%000001 // [fsm_arc t.nextstate_sel_ok_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000001 // [fsm_arc t.nextstate_sel_ok_u.state_q::S1->S2] +%000001 // [fsm_state t.nextstate_sel_ok_u.state_q::S0] +%000001 // [fsm_state t.nextstate_sel_ok_u.state_q::S1] +%000001 // [fsm_state t.nextstate_sel_ok_u.state_q::S2] +%000008 S0: if (start) state_d = S1; +%000001 S1: state_d = S2; +%000001 default: state_d = S0; + endcase + end + +%000009 always_ff @(posedge clk) begin +%000007 if (rst) state_q <= S0; +%000007 else state_q <= state_d; + end + endmodule + + module fsm_ternary ( +%000009 input logic clk, +%000001 input logic rst, +%000001 input logic sel + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + +%000004 state_t state_q; +%000004 state_t state_d; + + 000010 always_comb begin + 000010 state_d = state_q; +~000010 case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.ternary_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000000 // [fsm_arc t.ternary_u.state_q::S0->S1] +%000004 // [fsm_arc t.ternary_u.state_q::S0->S2] +%000003 // [fsm_state t.ternary_u.state_q::S0] +%000000 // [fsm_state t.ternary_u.state_q::S1] *** UNCOVERED *** +%000004 // [fsm_state t.ternary_u.state_q::S2] +%000009 S0: state_d = sel ? S1 : S2; +%000004 default: state_d = S0; + endcase + end + +%000009 always_ff @(posedge clk) begin +%000007 if (rst) state_q <= S0; +%000007 else state_q <= state_d; + end + endmodule + + module fsm_plain_always ( +%000009 input logic clk, +%000001 input logic rst, +%000001 input logic go + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + +%000001 state_t state_q; +%000001 state_t state_d; + + 000010 always @* begin + 000010 state_d = state_q; +~000010 case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.plain_always_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000001 // [fsm_arc t.plain_always_u.state_q::S1->S2] +%000001 // [fsm_state t.plain_always_u.state_q::S0] +%000001 // [fsm_state t.plain_always_u.state_q::S1] +%000001 // [fsm_state t.plain_always_u.state_q::S2] +%000008 S0: if (go) state_d = S1; +%000001 S1: state_d = S2; +%000001 default: state_d = S0; + endcase + end + +%000009 always_ff @(posedge clk) begin +%000007 if (rst) state_q <= S0; +%000007 else state_q <= state_d; + end + endmodule + + module fsm_plain_always_list ( +%000009 input logic clk, +%000001 input logic rst, +%000001 input logic go + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + +%000004 state_t state_q; +%000004 state_t state_d; + +%000008 always @(state_q or go) begin +%000008 state_d = state_q; +%000008 case (state_q) + // [FSM coverage] +%000001 // [fsm_arc t.plain_always_list_u.state_q::ANY->S0[reset]] [reset arc, excluded from %] +%000000 // [fsm_arc t.plain_always_list_u.state_q::S0->S1] +%000004 // [fsm_arc t.plain_always_list_u.state_q::S0->S2] +%000003 // [fsm_state t.plain_always_list_u.state_q::S0] +%000000 // [fsm_state t.plain_always_list_u.state_q::S1] *** UNCOVERED *** +%000004 // [fsm_state t.plain_always_list_u.state_q::S2] +%000007 S0: state_d = go ? S1 : S2; +%000004 default: state_d = S0; + endcase + end + +%000009 always_ff @(posedge clk) begin +%000007 if (rst) state_q <= S0; +%000007 else state_q <= state_d; + end + endmodule + + module fsm_caseassigns_off ( +%000009 input logic clk, +%000001 input logic rst, +%000001 input logic go + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + +%000001 logic out; +%000000 state_t state_q; +%000000 state_t state_d; + + 000010 always_comb begin + 000010 state_d = state_q; + 000010 out = 1'b0; + 000010 case (state_q) + 000010 S0: out = go; +%000000 default: out = 1'b0; + endcase + end + +%000009 always_ff @(posedge clk) begin +%000007 if (rst) state_q <= S0; +%000007 else state_q <= state_d; + end + endmodule + + module fsm_seqmix_off ( +%000009 input logic clk, +%000001 input logic rst + ); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + +%000004 logic side; +%000004 state_t state_q; +%000004 state_t state_d; + +%000001 initial side = 1'b0; + + 000010 always_comb begin + 000010 state_d = state_q; + 000010 case (state_q) +%000006 S0: state_d = S1; +%000004 default: state_d = S0; + endcase + end + +%000009 always_ff @(posedge clk) begin +%000007 if (rst) state_q <= S0; +%000007 else begin +%000007 side <= ~side; +%000007 state_q <= state_d; + end + end + endmodule + + module t ( +%000009 input clk + ); + integer cyc; +%000001 logic rst; +%000001 logic start; +%000001 logic bit_done; +%000001 logic sel; + + fsm_basic basic_u (.clk(clk), .rst(rst), .start(start)); + fsm_three_block three_block_u (.clk(clk), .rst(rst), .start(start)); + fsm_mealy mealy_u (.clk(clk), .rst(rst), .start(start), .bit_done(bit_done)); + fsm_reset_policy reset_u (.clk(clk), .rst(rst)); + fsm_nextstate_sel_off nextstate_sel_off_u (.clk(clk)); + fsm_nextstate_sel_ok nextstate_sel_ok_u (.clk(clk), .rst(rst), .start(start)); + fsm_ternary ternary_u (.clk(clk), .rst(rst), .sel(sel)); + fsm_plain_always plain_always_u (.clk(clk), .rst(rst), .go(start)); + fsm_plain_always_list plain_always_list_u (.clk(clk), .rst(rst), .go(start)); + fsm_caseassigns_off caseassigns_off_u (.clk(clk), .rst(rst), .go(start)); + fsm_seqmix_off seqmix_off_u (.clk(clk), .rst(rst)); + +%000001 initial begin +%000001 cyc = 0; +%000001 rst = 1'b1; +%000001 start = 1'b0; +%000001 bit_done = 1'b0; +%000001 sel = 1'b0; + end + +%000009 always @(posedge clk) begin +%000009 cyc <= cyc + 1; +%000008 if (cyc == 1) rst <= 1'b0; +%000008 if (cyc == 2) start <= 1'b1; +%000008 if (cyc == 3) start <= 1'b0; +%000008 if (cyc == 4) bit_done <= 1'b1; +%000008 if (cyc == 4) sel <= 1'b1; +%000008 if (cyc == 5) bit_done <= 1'b0; +%000008 if (cyc == 5) sel <= 1'b0; +%000008 if (cyc == 8) begin +%000001 $write("*-* All Finished *-*\n"); +%000001 $finish; + end + end + endmodule + diff --git a/test_regress/t/t_cover_fsm_two_proc_multi.py b/test_regress/t/t_cover_fsm_two_proc_multi.py new file mode 100755 index 000000000..0768245c3 --- /dev/null +++ b/test_regress/t/t_cover_fsm_two_proc_multi.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: FSM coverage combined two-process/three-block regression +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import os + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--cc --coverage']) + +test.execute() + +test.run(cmd=[ + os.environ["VERILATOR_ROOT"] + "/bin/verilator_coverage", + "--annotate", + test.obj_dir + "/annotated", + test.obj_dir + "/coverage.dat", +], + verilator_run=True) + +test.files_identical(test.obj_dir + "/annotated/" + test.name + ".v", test.golden_filename) +test.file_grep_not(test.obj_dir + "/coverage.dat", r"t\.nextstate_sel_off_u\.state_q.*fsm_") +test.file_grep_not(test.obj_dir + "/coverage.dat", r"t\.caseassigns_off_u\.state_q.*fsm_") +test.file_grep_not(test.obj_dir + "/coverage.dat", r"t\.seqmix_off_u\.state_q.*fsm_") + +test.passes() diff --git a/test_regress/t/t_cover_fsm_two_proc_multi.v b/test_regress/t/t_cover_fsm_two_proc_multi.v new file mode 100644 index 000000000..6cb3c7354 --- /dev/null +++ b/test_regress/t/t_cover_fsm_two_proc_multi.v @@ -0,0 +1,401 @@ +// DESCRIPTION: Verilator: FSM coverage combined two-process/three-block regression +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// Group supported multi-process and nearby supported-core variants so the +// main two-process / three-block extraction family stays in one place. + +module fsm_basic ( + input logic clk, + input logic rst, + input logic start +); + typedef enum logic [1:0] { + S_IDLE = 2'd0, + S_RUN = 2'd1, + S_DONE = 2'd2, + S_ERR = 2'd3 + } state_t; + + state_t state_q /*verilator fsm_reset_arc*/; + state_t state_d; + + always_comb begin + state_d = state_q; + case (state_q) + S_IDLE: if (start) state_d = S_RUN; + S_RUN: state_d = S_DONE; + default: state_d = S_ERR; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S_IDLE; + else state_q <= state_d; + end +endmodule + +module fsm_three_block ( + input logic clk, + input logic rst, + input logic start +); + typedef enum logic [1:0] { + S_IDLE = 2'd0, + S_BUSY = 2'd1, + S_DONE = 2'd2 + } state_t; + + logic busy; + logic done; + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + case (state_q) + S_IDLE: if (start) state_d = S_BUSY; + S_BUSY: state_d = S_DONE; + S_DONE: state_d = S_IDLE; + default: state_d = S_IDLE; + endcase + end + + always_comb begin + busy = (state_q == S_BUSY); + done = (state_q == S_DONE); + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S_IDLE; + else state_q <= state_d; + end +endmodule + +module fsm_mealy ( + input logic clk, + input logic rst, + input logic start, + input logic bit_done +); + typedef enum logic [1:0] { + S_IDLE = 2'd0, + S_SEND = 2'd1, + S_DONE = 2'd2 + } state_t; + + logic tx; + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + tx = 1'b1; + case (state_q) + S_IDLE: begin + if (start) begin + state_d = S_SEND; + tx = 1'b0; + end + end + S_SEND: begin + tx = bit_done; + if (bit_done) state_d = S_DONE; + end + S_DONE: state_d = S_IDLE; + default: state_d = S_IDLE; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S_IDLE; + else state_q <= state_d; + end +endmodule + +module fsm_reset_policy ( + input logic clk, + input logic rst +); + typedef enum logic [0:0] { + S0 = 1'b0, + S1 = 1'b1 + } state_t; + + state_t state_incl_q /*verilator fsm_reset_arc*/; + state_t state_incl_d; + state_t state_excl_q; + state_t state_excl_d; + + always_comb begin + state_incl_d = state_incl_q; + case (state_incl_q) + S0: state_incl_d = S1; + default: state_incl_d = S1; + endcase + end + + always_comb begin + state_excl_d = state_excl_q; + case (state_excl_q) + S0: state_excl_d = S1; + default: state_excl_d = S1; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_incl_q <= S0; + else state_incl_q <= state_incl_d; + end + + always_ff @(posedge clk) begin + if (rst) state_excl_q <= S0; + else state_excl_q <= state_excl_d; + end +endmodule + +module fsm_nextstate_sel_off ( + input logic clk +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = S0; + case (state_d) + S0: state_d = S1; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + state_q <= state_d; + end +endmodule + +module fsm_nextstate_sel_ok ( + input logic clk, + input logic rst, + input logic start +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + case (state_d) + S0: if (start) state_d = S1; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule + +module fsm_ternary ( + input logic clk, + input logic rst, + input logic sel +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + case (state_q) + S0: state_d = sel ? S1 : S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule + +module fsm_plain_always ( + input logic clk, + input logic rst, + input logic go +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always @* begin + state_d = state_q; + case (state_q) + S0: if (go) state_d = S1; + S1: state_d = S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule + +module fsm_plain_always_list ( + input logic clk, + input logic rst, + input logic go +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1, + S2 = 2'd2 + } state_t; + + state_t state_q; + state_t state_d; + + always @(state_q or go) begin + state_d = state_q; + case (state_q) + S0: state_d = go ? S1 : S2; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule + +module fsm_caseassigns_off ( + input logic clk, + input logic rst, + input logic go +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + + logic out; + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + out = 1'b0; + case (state_q) + S0: out = go; + default: out = 1'b0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else state_q <= state_d; + end +endmodule + +module fsm_seqmix_off ( + input logic clk, + input logic rst +); + typedef enum logic [1:0] { + S0 = 2'd0, + S1 = 2'd1 + } state_t; + + logic side; + state_t state_q; + state_t state_d; + + initial side = 1'b0; + + always_comb begin + state_d = state_q; + case (state_q) + S0: state_d = S1; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + if (rst) state_q <= S0; + else begin + side <= ~side; + state_q <= state_d; + end + end +endmodule + +module t ( + input clk +); + integer cyc; + logic rst; + logic start; + logic bit_done; + logic sel; + + fsm_basic basic_u (.clk(clk), .rst(rst), .start(start)); + fsm_three_block three_block_u (.clk(clk), .rst(rst), .start(start)); + fsm_mealy mealy_u (.clk(clk), .rst(rst), .start(start), .bit_done(bit_done)); + fsm_reset_policy reset_u (.clk(clk), .rst(rst)); + fsm_nextstate_sel_off nextstate_sel_off_u (.clk(clk)); + fsm_nextstate_sel_ok nextstate_sel_ok_u (.clk(clk), .rst(rst), .start(start)); + fsm_ternary ternary_u (.clk(clk), .rst(rst), .sel(sel)); + fsm_plain_always plain_always_u (.clk(clk), .rst(rst), .go(start)); + fsm_plain_always_list plain_always_list_u (.clk(clk), .rst(rst), .go(start)); + fsm_caseassigns_off caseassigns_off_u (.clk(clk), .rst(rst), .go(start)); + fsm_seqmix_off seqmix_off_u (.clk(clk), .rst(rst)); + + initial begin + cyc = 0; + rst = 1'b1; + start = 1'b0; + bit_done = 1'b0; + sel = 1'b0; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 1) rst <= 1'b0; + if (cyc == 2) start <= 1'b1; + if (cyc == 3) start <= 1'b0; + if (cyc == 4) bit_done <= 1'b1; + if (cyc == 4) sel <= 1'b1; + if (cyc == 5) bit_done <= 1'b0; + if (cyc == 5) sel <= 1'b0; + if (cyc == 8) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end +endmodule diff --git a/test_regress/t/t_cover_otherdecl_dump.py b/test_regress/t/t_cover_otherdecl_dump.py index a1524c584..a82316eea 100755 --- a/test_regress/t/t_cover_otherdecl_dump.py +++ b/test_regress/t/t_cover_otherdecl_dump.py @@ -12,7 +12,7 @@ from pathlib import Path import vltest_bootstrap test.scenarios('vlt') -test.top_filename = "t/t_cover_fsm_styles.v" +test.top_filename = "t/t_cover_fsm_policy_accept_multi.v" # Dump generic COVEROTHERDECL nodes so AstCoverOtherDecl::dump() also sees # coverage declarations with no FSM metadata, exercising the empty-field side diff --git a/test_regress/t/t_fsmmulti_combo_multi_warn_bad.out b/test_regress/t/t_fsmmulti_combo_multi_warn_bad.out new file mode 100644 index 000000000..9d16b62e2 --- /dev/null +++ b/test_regress/t/t_fsmmulti_combo_multi_warn_bad.out @@ -0,0 +1,15 @@ +%Warning-FSMMULTI: t/t_fsmmulti_combo_multi_warn_bad.v:34:21: FSM coverage: multiple supported transition candidates found in the same combinational always block. Only the first candidate will be instrumented. + 34 | B0: state_b_d = B1; + | ^ + t/t_fsmmulti_combo_multi_warn_bad.v:30:21: ... Location of first supported candidate for 't.same_u.state_a_q' + 30 | A0: state_a_d = A1; + | ^ + ... For warning description see https://verilator.org/warn/FSMMULTI?v=latest + ... Use "/* verilator lint_off FSMMULTI */" and lint_on around source to disable this message. +%Warning-FSMMULTI: t/t_fsmmulti_combo_multi_warn_bad.v:70:19: FSM coverage: multiple supported transition candidates found for the same FSM in combinational always blocks. Only the first candidate will be instrumented. + 70 | S0: state_d = S1; + | ^ + t/t_fsmmulti_combo_multi_warn_bad.v:62:19: ... Location of first supported candidate for 't.split_u.state_q' + 62 | S0: state_d = S1; + | ^ +%Error: Exiting due to diff --git a/test_regress/t/t_fsmmulti_combo_multi_warn_bad.py b/test_regress/t/t_fsmmulti_combo_multi_warn_bad.py new file mode 100755 index 000000000..9359d4535 --- /dev/null +++ b/test_regress/t/t_fsmmulti_combo_multi_warn_bad.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: combined FSMMULTI warning regression +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.lint(verilator_flags2=["--coverage-fsm"], fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_fsmmulti_combo_multi_warn_bad.v b/test_regress/t/t_fsmmulti_combo_multi_warn_bad.v new file mode 100644 index 000000000..f21c56bf9 --- /dev/null +++ b/test_regress/t/t_fsmmulti_combo_multi_warn_bad.v @@ -0,0 +1,86 @@ +// DESCRIPTION: Verilator: combined FSMMULTI warning regression +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// Group the combo-family multi-candidate warnings where two supported +// transition sites compete and the detector must keep only the first one. + +module same_always_warn ( + input logic clk +); + typedef enum logic [1:0] { + A0, A1 + } a_state_t; + + typedef enum logic [1:0] { + B0, B1 + } b_state_t; + + a_state_t state_a_q; + a_state_t state_a_d; + b_state_t state_b_q; + b_state_t state_b_d; + + always_comb begin + state_a_d = state_a_q; + state_b_d = state_b_q; + case (state_a_q) + A0: state_a_d = A1; + default: state_a_d = A0; + endcase + case (state_b_q) + B0: state_b_d = B1; + default: state_b_d = B0; + endcase + end + + always_ff @(posedge clk) begin + state_a_q <= state_a_d; + end + + always_ff @(posedge clk) begin + state_b_q <= state_b_d; + end +endmodule + +module split_always_warn ( + input logic clk +); + /* verilator lint_off MULTIDRIVEN */ + typedef enum logic [1:0] { + S0, S1 + } state_t; + + state_t state_q; + state_t state_d; + + always_comb begin + state_d = state_q; + case (state_q) + S0: state_d = S1; + default: state_d = S0; + endcase + end + + always_comb begin + state_d = state_q; + case (state_q) + S0: state_d = S1; + default: state_d = S0; + endcase + end + + always_ff @(posedge clk) begin + state_q <= state_d; + end + /* verilator lint_on MULTIDRIVEN */ +endmodule + +module t ( + input logic clk +); + same_always_warn same_u (.clk(clk)); + split_always_warn split_u (.clk(clk)); +endmodule diff --git a/test_regress/t/t_fsmmulti_same_bad.out b/test_regress/t/t_fsmmulti_same_bad.out index d48efde98..1efc9fc77 100644 --- a/test_regress/t/t_fsmmulti_same_bad.out +++ b/test_regress/t/t_fsmmulti_same_bad.out @@ -1,6 +1,9 @@ %Warning-COVERIGN: t/t_fsmmulti_same_bad.v:33:5: Ignoring unsupported: FSM coverage on multiple supported case statements found in the same always block. Only the first candidate will be instrumented. 33 | case (state) | ^~~~ + t/t_fsmmulti_same_bad.v:28:7: ... Location of first supported candidate for 't.state' + 28 | case (state) + | ^~~~ ... For warning description see https://verilator.org/warn/COVERIGN?v=latest ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. %Error: Exiting due to diff --git a/test_regress/t/t_fsmmulti_warn_bad.out b/test_regress/t/t_fsmmulti_warn_bad.out index 27e284264..82d64e44b 100644 --- a/test_regress/t/t_fsmmulti_warn_bad.out +++ b/test_regress/t/t_fsmmulti_warn_bad.out @@ -1,5 +1,8 @@ %Warning-FSMMULTI: t/t_fsmmulti_warn_bad.v:29:5: FSM coverage: multiple enum-typed case statements found in the same always block. Only the first candidate will be instrumented. 29 | case (state_b) + | ^~~~ + t/t_fsmmulti_warn_bad.v:25:5: ... Location of first supported candidate for 't.state_a' + 25 | case (state_a) | ^~~~ ... For warning description see https://verilator.org/warn/FSMMULTI?v=latest ... Use "/* verilator lint_off FSMMULTI */" and lint_on around source to disable this message. diff --git a/test_regress/t/t_vlcov_fsm_report.out b/test_regress/t/t_vlcov_fsm_report.out index 1c5e71b72..d6598b075 100644 --- a/test_regress/t/t_vlcov_fsm_report.out +++ b/test_regress/t/t_vlcov_fsm_report.out @@ -50,6 +50,8 @@ %000005 case (state_default) // [FSM coverage] %000001 // [fsm_arc t.state_default::ANY->S0[reset]] [reset arc, excluded from %] +%000000 // [fsm_arc t.state_default::S0->S1] +%000003 // [fsm_arc t.state_default::S0->S2] %000000 // [SYNTHETIC DEFAULT ARC: t.state_default::default->S0] %000002 // [fsm_state t.state_default::S0] %000000 // [fsm_state t.state_default::S1] *** UNCOVERED *** diff --git a/test_regress/t/t_vlcov_fsm_report_incl.out b/test_regress/t/t_vlcov_fsm_report_incl.out index ccf63b0b1..31117beb4 100644 --- a/test_regress/t/t_vlcov_fsm_report_incl.out +++ b/test_regress/t/t_vlcov_fsm_report_incl.out @@ -50,6 +50,8 @@ %000005 case (state_default) // [FSM coverage] %000001 // [fsm_arc t.state_default::ANY->S0[reset]] +%000000 // [fsm_arc t.state_default::S0->S1] +%000003 // [fsm_arc t.state_default::S0->S2] %000000 // [SYNTHETIC DEFAULT ARC: t.state_default::default->S0] %000002 // [fsm_state t.state_default::S0] %000000 // [fsm_state t.state_default::S1] *** UNCOVERED *** From 021f5f7d02fd42bfafc4acd7e93e63d559334acc Mon Sep 17 00:00:00 2001 From: github action Date: Thu, 30 Apr 2026 11:23:36 +0000 Subject: [PATCH 023/350] Apply 'make format' --- src/V3FsmDetect.cpp | 39 +++++++++---------- .../t_cover_fsm_plain_always_zerohit_multi.py | 0 2 files changed, 19 insertions(+), 20 deletions(-) mode change 100644 => 100755 test_regress/t/t_cover_fsm_plain_always_zerohit_multi.py diff --git a/src/V3FsmDetect.cpp b/src/V3FsmDetect.cpp index f09f91163..2216f8521 100644 --- a/src/V3FsmDetect.cpp +++ b/src/V3FsmDetect.cpp @@ -354,16 +354,17 @@ class FsmDetectVisitor final : public VNVisitor { explicit RegisterAlwaysAnalyzer(AstScope* scopep) : m_scopep{scopep} {} - std::vector> oneBlockCandidates(AstAlways* alwaysp) const { + std::vector> + oneBlockCandidates(AstAlways* alwaysp) const { std::vector> candidates; AstNode* const stmtsp = alwaysp->stmtsp(); AstIf* const firstIfp = VN_CAST(stmtsp, If); if (firstIfp) { if (AstCase* const casep = VN_CAST(firstIfp->elsesp(), Case)) { - candidates.emplace_back( - casep, FsmDetectVisitor::isSimpleResetCond(firstIfp->condp()) - ? firstIfp->condp() - : nullptr); + candidates.emplace_back(casep, + FsmDetectVisitor::isSimpleResetCond(firstIfp->condp()) + ? firstIfp->condp() + : nullptr); } } for (AstNode* nodep = stmtsp; nodep; nodep = nodep->nextp()) { @@ -377,8 +378,8 @@ class FsmDetectVisitor final : public VNVisitor { return FsmDetectVisitor::matchRegisterAlways(alwaysp, m_scopep, cand); } - void buildOneBlockCandidate(AstAlways* alwaysp, AstVarScope* vscp, - AstNodeExpr* resetCondp, FsmRegisterCandidate& reg) const { + void buildOneBlockCandidate(AstAlways* alwaysp, AstVarScope* vscp, AstNodeExpr* resetCondp, + FsmRegisterCandidate& reg) const { reg.scopep(m_scopep); reg.alwaysp(alwaysp); reg.stateVscp(vscp); @@ -441,9 +442,8 @@ class FsmDetectVisitor final : public VNVisitor { } else if (selp->varScopep() != reg.stateVscp()) { continue; } - AstNode* const warnNodep - = FsmDetectVisitor::caseSupportedTransitionNode(casep, reg.nextVscp(), - reg.inclCond()); + AstNode* const warnNodep = FsmDetectVisitor::caseSupportedTransitionNode( + casep, reg.nextVscp(), reg.inclCond()); if (!warnNodep) continue; match.matchedp = ® match.warnNodep = warnNodep; @@ -1034,16 +1034,16 @@ class FsmDetectVisitor final : public VNVisitor { if (!matchedp) continue; if (!firstCand.stateVscp) { const auto insertPair = m_comboPaired.emplace( - matchedp->stateVscp(), FsmCaseCandidate{matchedWarnNodep, - const_cast( - matchedp->stateVscp())}); + matchedp->stateVscp(), + FsmCaseCandidate{matchedWarnNodep, + const_cast(matchedp->stateVscp())}); if (!insertPair.second) { matchedWarnNodep->v3warn( - FSMMULTI, - "FSM coverage: multiple supported transition candidates found " - "for the same FSM in combinational always blocks. Only the " - "first candidate will be instrumented." - << candidateConflictContext(matchedWarnNodep, insertPair.first->second)); + FSMMULTI, "FSM coverage: multiple supported transition candidates found " + "for the same FSM in combinational always blocks. Only the " + "first candidate will be instrumented." + << candidateConflictContext(matchedWarnNodep, + insertPair.first->second)); continue; } firstCand.warnNodep = matchedWarnNodep; @@ -1060,8 +1060,7 @@ class FsmDetectVisitor final : public VNVisitor { "supported case statements found in the same " "combinational always block. Only the first " "candidate will be instrumented." - << candidateConflictContext(matchedWarnNodep, - firstCand)); + << candidateConflictContext(matchedWarnNodep, firstCand)); } } diff --git a/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.py b/test_regress/t/t_cover_fsm_plain_always_zerohit_multi.py old mode 100644 new mode 100755 From dd75c4cd1b5b301723fa8296e317842a8fe193a7 Mon Sep 17 00:00:00 2001 From: Wilson Snyder Date: Thu, 30 Apr 2026 10:29:47 -0400 Subject: [PATCH 024/350] Revert: 21020ea2: Support printing enum names for %p and %s (#5523 revert) (#7338 revert) (#7521 test) --- include/verilated.cpp | 48 ++----- include/verilatedos.h | 1 - src/V3EmitCFunc.cpp | 2 +- src/V3Number.h | 2 - src/V3Width.cpp | 57 +------- test_regress/t/t_class_enum.v | 26 ---- test_regress/t/t_display_enum_format.py | 18 --- test_regress/t/t_display_enum_format.v | 167 ------------------------ test_regress/t/t_enum_huge_methods.v | 8 +- test_regress/t/t_enum_large_methods.v | 6 +- test_regress/t/t_enum_type_methods.v | 52 +++----- 11 files changed, 31 insertions(+), 356 deletions(-) delete mode 100755 test_regress/t/t_display_enum_format.py delete mode 100644 test_regress/t/t_display_enum_format.v diff --git a/include/verilated.cpp b/include/verilated.cpp index 942d2877f..3e20be69d 100644 --- a/include/verilated.cpp +++ b/include/verilated.cpp @@ -944,20 +944,6 @@ std::string _vl_vsformat_time(std::string& tmp, T ld, int timeunit, bool left, // Do a va_arg returning a quad, assuming input argument is anything less than wide #define VL_VA_ARG_Q_(ap, bits) (((bits) <= VL_IDATASIZE) ? va_arg(ap, IData) : va_arg(ap, QData)) -static void _vl_vsformat_read_qint(va_list app, int lbits, QData& ld, std::vector& strwide, - WDataInP& lwp, int& lsb) VL_MT_SAFE { - if (lbits <= VL_QUADSIZE) { - ld = VL_VA_ARG_Q_(app, lbits); - strwide.resize(2); - VL_SET_WQ(strwide, ld); - lwp = strwide.data(); - } else { - lwp = va_arg(app, WDataInP); - ld = 0; // Consume the arg; enums > 64 bits wide are unsupported. - } - lsb = lbits - 1; -} - void _vl_vsformat(std::string& output, const std::string& format, int argc, va_list ap) VL_MT_SAFE { // Format a Verilog $write style format into the output list @@ -1092,7 +1078,6 @@ void _vl_vsformat(std::string& output, const std::string& format, int argc, // Similar code flow in V3Number::displayed int lbits = 0; void* thingp = nullptr; - const std::string* enump = nullptr; QData ld = 0; std::vector strwide; WDataInP lwp = nullptr; @@ -1112,31 +1097,17 @@ void _vl_vsformat(std::string& output, const std::string& format, int argc, } else if (formatAttr == VL_VFORMATATTR_STRING) { thingp = va_arg(ap, std::string*); if (fmt != 'p' && fmt != 'x') fmt = 's'; // Override - } else if (formatAttr == VL_VFORMATATTR_ENUM) { - lbits = va_arg(ap, int); - _vl_vsformat_read_qint(ap, lbits, ld, strwide, lwp, lsb); - ++argn; // Internal ABI: runtime enum args are followed by generated name string - static_cast(va_arg(ap, int)); // VL_VFORMATATTR_STRING - enump = va_arg(ap, std::string*); - if (enump && !enump->empty()) { - formatAttr = (fmt == 'p') ? VL_VFORMATATTR_COMPLEX : VL_VFORMATATTR_STRING; - thingp = const_cast(enump); - } else if (fmt == 'p' && widthSet && width == 0) { - output += "'h"; - fmt = 'h'; - formatAttr = VL_VFORMATATTR_UNSIGNED; - } else { - if (fmt == 'p') width = 0; - widthSet = true; - fmt = 'd'; - formatAttr = VL_VFORMATATTR_UNSIGNED; - } - if (widthSet && width == 0) { - while (lsb && !VL_BITISSET_W(lwp, lsb)) --lsb; - } } else { // Numeric lbits = va_arg(ap, int); - _vl_vsformat_read_qint(ap, lbits, ld, strwide, lwp, lsb); + if (lbits <= VL_QUADSIZE) { + ld = VL_VA_ARG_Q_(ap, lbits); + strwide.resize(2); + VL_SET_WQ(strwide, ld); + lwp = strwide.data(); + } else { + lwp = va_arg(ap, WDataInP); + ld = lwp[0]; + } if (fmt == 'p') { if (widthSet && width == 0) { // For %0p, IEEE our choice, use 'h%0h output += "'h"; @@ -1147,6 +1118,7 @@ void _vl_vsformat(std::string& output, const std::string& format, int argc, fmt = 'd'; } } + lsb = lbits - 1; if (widthSet && width == 0) { while (lsb && !VL_BITISSET_W(lwp, lsb)) --lsb; } diff --git a/include/verilatedos.h b/include/verilatedos.h index 3db48823a..b93909309 100644 --- a/include/verilatedos.h +++ b/include/verilatedos.h @@ -442,7 +442,6 @@ using ssize_t = uint32_t; ///< signed size_t; returned from read() #define VL_VFORMATATTR_SIGNED '~' // (int widthMin, IData/WData/etc) Signed number; for %d showing sign #define VL_VFORMATATTR_COMPLEX '!' // (std::string*); for non-POD; e.g. struct, requires %p typically #define VL_VFORMATATTR_DOUBLE 'D' // (double); promote %p to %f -#define VL_VFORMATATTR_ENUM 'E' // (width, data..., std::string* name); enum with runtime %p/%s #define VL_VFORMATATTR_SCOPE 'M' // (char* name, char* scope); for scopes #define VL_VFORMATATTR_STRING 'S' // (char* name, char* scope); for scopes // (std::string*); for %p/%s #define VL_VFORMATATTR_TIMEUNIT 'T' // (int timeunit); timeunits passed from V3Emit to runtime diff --git a/src/V3EmitCFunc.cpp b/src/V3EmitCFunc.cpp index a18e09deb..f324623a0 100644 --- a/src/V3EmitCFunc.cpp +++ b/src/V3EmitCFunc.cpp @@ -318,7 +318,7 @@ void EmitCFunc::displayNode(AstNode* nodep, AstSFormatF* fmtp, // fmtp is nullp AstNode* const subargp = fargp ? fargp->exprp() : argp; const VFormatAttr formatAttr = AstSFormatArg::formatAttrDefauled(fargp, subargp->dtypep()); puts(", '"s + formatAttr.ascii() + '\''); - if (formatAttr.isSigned() || formatAttr.isUnsigned() || formatAttr.isEnum()) + if (formatAttr.isSigned() || formatAttr.isUnsigned()) puts("," + cvtToStr(subargp->widthMin())); const bool addrof = isScan || formatAttr.isString() || formatAttr.isComplex(); puts(","); diff --git a/src/V3Number.h b/src/V3Number.h index 031554a75..c5d1a57bf 100644 --- a/src/V3Number.h +++ b/src/V3Number.h @@ -46,7 +46,6 @@ public: // COMPLEX = VL_VFORMATATTR_COMPLEX, DOUBLE = VL_VFORMATATTR_DOUBLE, - ENUM = VL_VFORMATATTR_ENUM, SCOPE = VL_VFORMATATTR_SCOPE, STRING = VL_VFORMATATTR_STRING, TIMEUNIT = VL_VFORMATATTR_TIMEUNIT @@ -63,7 +62,6 @@ public: char ascii() const { return m_e; } bool isComplex() const { return m_e == COMPLEX; } bool isDouble() const { return m_e == DOUBLE; } - bool isEnum() const { return m_e == ENUM; } bool isSigned() const { return m_e == SIGNED; } bool isString() const { return m_e == STRING; } bool isUnsigned() const { return m_e == UNSIGNED; } diff --git a/src/V3Width.cpp b/src/V3Width.cpp index 6cb6959bd..713388321 100644 --- a/src/V3Width.cpp +++ b/src/V3Width.cpp @@ -6364,17 +6364,7 @@ class WidthVisitor final : public VNVisitor { AstNodeExpr* const newp = new AstToStringN{argp->fileline(), argp}; formatAttr = VFormatAttr::COMPLEX; argp = newp; - } else if (nodep->exprFormat()) { - if (AstEnumDType* const enumDtp = formatEnumDType(argp)) { - nodep->addExprsp(new AstSFormatArg{argp->fileline(), VFormatAttr::ENUM, argp}); - AstNodeExpr* const namep - = enumSelect(argp->cloneTreePure(false), enumDtp, VAttrType::ENUM_NAME); - nodep->addExprsp( - new AstSFormatArg{namep->fileline(), VFormatAttr::STRING, namep}); - continue; - } - } - if (formatAttr.isUnsigned() && dtypep->isSigned()) { + } else if (dtypep->isSigned()) { formatAttr = VFormatAttr::SIGNED; } if (VN_IS(argp, SFormatArg) // Already done @@ -8241,16 +8231,13 @@ class WidthVisitor final : public VNVisitor { // For sformatf's with constant format, iterate/check arguments UASSERT_OBJ(!nodep->exprFormat(), nodep, "Assumes constant format"); bool inPct = false; - string fmtMods; AstNodeExpr* argp = nodep->exprsp(); string newFormat; for (char ch : nodep->text()) { if (!inPct && ch == '%') { inPct = true; - fmtMods = ""; newFormat += ch; } else if (inPct && (std::isdigit(ch) || ch == '.' || ch == '-')) { - fmtMods += ch; newFormat += ch; } else if (!inPct) { // Normal text newFormat += ch; @@ -8258,7 +8245,7 @@ class WidthVisitor final : public VNVisitor { inPct = false; AstNodeExpr* const nextp = argp ? VN_AS(argp->nextp(), NodeExpr) : nullptr; AstSFormatArg* const fargp = VN_CAST(argp, SFormatArg); // May not exist yet - AstNodeExpr* subargp = fargp ? fargp->exprp() : argp; + AstNodeExpr* const subargp = fargp ? fargp->exprp() : argp; const AstNodeDType* const dtypep = subargp ? subargp->dtypep()->skipRefp() : nullptr; ch = std::tolower(ch); @@ -8315,35 +8302,7 @@ class WidthVisitor final : public VNVisitor { } break; case 'p': // FALLTHRU - case 's': - // Keep enum `%p`/`%s` behavior aligned with enum.name(): - // valid enum values print the mnemonic; invalid values print numeric fallback. - if (subargp) { - if (AstEnumDType* const enumDtp = formatEnumDType(subargp)) { - string fallbackFormat = "%0d"; - if (ch == 'p') { - bool widthSet = false; - size_t width = 0; - for (const char mod : fmtMods) { - if (!std::isdigit(mod)) continue; - widthSet = true; - width = width * 10 + (mod - '0'); - } - if (widthSet && width == 0) fallbackFormat = "'h%0h"; - } - AstNodeExpr* const newp = new AstCond{ - subargp->fileline(), enumTestValid(subargp, enumDtp), - enumSelect(subargp->cloneTreePure(false), enumDtp, - VAttrType::ENUM_NAME), - new AstSFormatF{subargp->fileline(), fallbackFormat, true, - subargp->cloneTreePure(false)}}; - subargp->replaceWith(new AstSFormatArg{subargp->fileline(), - VFormatAttr::COMPLEX, newp}); - VL_DO_DANGLING(pushDeletep(subargp), subargp); - } - } - argp = nextp; - break; + case 's': // FALLTHRU default: // Most operators, just move to next argument argp = nextp; break; @@ -8354,16 +8313,6 @@ class WidthVisitor final : public VNVisitor { nodep->text(newFormat); } - static AstEnumDType* formatEnumDType(AstNodeExpr* subargp) { - AstEnumDType* enumDtp = VN_CAST(subargp->dtypep()->skipRefToEnump(), EnumDType); - if (!enumDtp) { - if (const AstVarRef* const varrefp = VN_CAST(subargp, VarRef)) { - enumDtp = VN_CAST(varrefp->varp()->dtypep()->skipRefToEnump(), EnumDType); - } - } - return enumDtp; - } - //---------------------------------------------------------------------- // LOWER LEVEL WIDTH METHODS (none iterate) diff --git a/test_regress/t/t_class_enum.v b/test_regress/t/t_class_enum.v index 98c09b246..bd30cd2f8 100644 --- a/test_regress/t/t_class_enum.v +++ b/test_regress/t/t_class_enum.v @@ -4,11 +4,6 @@ // SPDX-FileCopyrightText: 2020 Wilson Snyder // SPDX-License-Identifier: CC0-1.0 -// verilog_format: off -`define stop $stop -`define checks(gotv, expv) do if ((gotv) != (expv)) begin $write("%%Error: %s:%0d: got='%s' exp='%s'\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); -// verilog_format: on - module t; class Cls; @@ -17,32 +12,11 @@ module t; B = 20, C = 30 } en_t; - en_t en; - endclass - - class WideCls; - typedef enum logic [95:0] { - A = 96'h1, - B = 96'h2 - } en_t; - en_t en; endclass initial begin Cls c; - WideCls w; - string s; if (c.A != 10) $stop; - c = new; - c.en = c.B; - if (c.en != 20) $stop; - s = $sformatf("%p", c); - `checks(s, "'{en:'h14}"); - - w = new; - w.en = w.B; - s = $sformatf("%p", w); - `checks(s, "'{en:'h2}"); $write("*-* All Finished *-*\n"); $finish; end diff --git a/test_regress/t/t_display_enum_format.py b/test_regress/t/t_display_enum_format.py deleted file mode 100755 index 8a938befd..000000000 --- a/test_regress/t/t_display_enum_format.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# DESCRIPTION: Verilator: Verilog Test driver/expect definition -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of either the GNU Lesser General Public License Version 3 -# or the Perl Artistic License Version 2.0. -# SPDX-FileCopyrightText: 2026 Wilson Snyder -# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 - -import vltest_bootstrap - -test.scenarios('simulator') - -test.compile() - -test.execute() - -test.passes() diff --git a/test_regress/t/t_display_enum_format.v b/test_regress/t/t_display_enum_format.v deleted file mode 100644 index 8eb60f76f..000000000 --- a/test_regress/t/t_display_enum_format.v +++ /dev/null @@ -1,167 +0,0 @@ -// DESCRIPTION: Verilator: Verilog Test module -// -// 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 string empty_no_opt -); - typedef enum logic [1:0] { - E0 = 0, - E1 = 1, - E2 = 2 - } my_e; - - typedef enum logic [63:0] { - W64A = 64'h1, - W64B = 64'h0000_0001_0000_0001 - } wide64_e; - - my_e e; - wide64_e e64; - logic [63:0] n64; -`define check(got, exp) do if ((got) != (exp)) begin \ - $write("%%Error: %s:%0d: got='%s' exp='%s'\n", `__FILE__, `__LINE__, got, exp); \ - $stop; \ - end while(0) - - initial begin - string fmt; - begin - my_e it; - string names_p; - string names_s; - string vals_d; - names_p = ""; - names_s = ""; - vals_d = ""; - for (it = it.first; ; it = it.next) begin - if (names_p != "") begin - names_p = {names_p, ","}; - names_s = {names_s, ","}; - vals_d = {vals_d, ","}; - end - names_p = {names_p, $sformatf("%p", it)}; - names_s = {names_s, $sformatf("%s", it)}; - vals_d = {vals_d, $sformatf("%0d", it)}; - if (it == it.last) break; - end - `check(names_p, "E0,E1,E2"); - `check(names_s, "E0,E1,E2"); - `check(vals_d, "0,1,2"); - end - - // Valid enum values print mnemonic for %p/%s. - e = E0; - `check($sformatf("%p", e), "E0"); - `check($sformatf("%s", e), "E0"); - - e = E1; - `check($sformatf("%p", e), "E1"); - `check($sformatf("%P", e), "E1"); - `check($sformatf("%0p", e), "E1"); - `check($sformatf("%s", e), "E1"); - `check($sformatf("%S", e), "E1"); - `check($sformatf("%d", e), "1"); - `check($sformatf("%0d", e), "1"); - `check($sformatf("%h", e), "1"); - `check($sformatf("%0h", e), "1"); - `check($sformatf("%b", e), "01"); - `check($sformatf("%0b", e), "1"); - `check($sformatf("%o", e), "1"); - `check($sformatf("%0o", e), "1"); - `check($sformatf("%x", e), "1"); - `check($sformatf("%0x", e), "1"); - - e = E2; - `check($sformatf("%p", e), "E2"); - `check($sformatf("%s", e), "E2"); - `check($sformatf("%s|%p", e, e), "E2|E2"); - `check($sformatf("%4p", e), "E2"); - `check($sformatf("%-4p", e), "E2"); - `check($sformatf("%d", e), "2"); - `check($sformatf("%h", e), "2"); - `check($sformatf("%b", e), "10"); - `check($sformatf("%0b", e), "10"); - `check($sformatf("%o", e), "2"); - `check($sformatf("%x", e), "2"); - `check($sformatf("%4d", e), " 2"); - `check($sformatf("%04d", e), "0002"); - `check($sformatf("%4h", e), "0002"); - `check($sformatf("%-4s", e), "E2 "); - `check($sformatf("%4s", e), " E2"); - // `%p`/`%s` in non-terminal positions with mixed formatters. - `check($sformatf("%0d:%s:%0d", 9, e, 7), "9:E2:7"); - `check($sformatf("%s %h %p", e, 4'hA, e), "E2 a E2"); - `check($sformatf("pre %% %s post", e), "pre % E2 post"); - // Complex enum expressions (non-var-ref) in format args. - `check($sformatf("%s", (1'b1 ? E2 : E0)), "E2"); - // 64-bit enums should preserve bits above 32 in both named and numeric cases. - e64 = W64B; - `check($sformatf("%p", e64), "W64B"); - `check($sformatf("%s", e64), "W64B"); - e64 = wide64_e'(64'h0000_0002_0000_0001); - `check($sformatf("%p", e64), "8589934593"); - `check($sformatf("%s", e64), "8589934593"); - n64 = 64'h0000_0000_0000_0001; - `check($sformatf("%0p", n64), "'h1"); - // Exercise display/write-family formatting path in addition to $sformatf checks. - $display("display-valid:%s:%0d:%p", e, 7, e); - $write("write-valid:%s:%0d:%p\n", e, 8, e); - // Invalid enum values fall back to numeric formatting for %p/%s. - e = my_e'(3); - `check($sformatf("%p", e), "3"); - `check($sformatf("%P", e), "3"); - `check($sformatf("%0p", e), "'h3"); - `check($sformatf("%s", e), "3"); - `check($sformatf("%S", e), "3"); - `check($sformatf("%4p", e), "3"); - `check($sformatf("%4s", e), " 3"); - `check($sformatf("%d", e), "3"); - `check($sformatf("%0d", e), "3"); - `check($sformatf("%h", e), "3"); - `check($sformatf("%0h", e), "3"); - `check($sformatf("%b", e), "11"); - `check($sformatf("%0b", e), "11"); - `check($sformatf("%o", e), "3"); - `check($sformatf("%x", e), "3"); - // Non-terminal invalid-value fallback with mixed formatters. - `check($sformatf("%0d:%p:%0d", 9, e, 7), "9:3:7"); - `check($sformatf("%s %h %p", e, 4'hA, e), "3 a 3"); - `check($sformatf("pre %% %s post", e), "pre % 3 post"); - `check($sformatf("%s|%p", e, e), "3|3"); - `check($sformatf("%s", (1'b1 ? my_e'(3) : E0)), "3"); - `check($sformatf("%p", (1'b0 ? E0 : my_e'(3))), "3"); - $display("display-invalid:%s:%0d:%p", e, 7, e); - $write("write-invalid:%s:%0d:%p\n", e, 8, e); - // Runtime-computed $sformatf formats should preserve enum mnemonic/fallback behavior. - e = E2; - fmt = {"%", "s", empty_no_opt}; - `check($sformatf(fmt, e), "E2"); - fmt = {"%", "p", empty_no_opt}; - `check($sformatf(fmt, e), "E2"); - fmt = {"%0d:%", "s", ":%0d", empty_no_opt}; - `check($sformatf(fmt, 9, e, 7), "9:E2:7"); - fmt = {"%", "s", " %h %", "p", empty_no_opt}; - `check($sformatf(fmt, e, 4'hA, e), "E2 a E2"); - e = my_e'(3); - fmt = {"%", "s", empty_no_opt}; - `check($sformatf(fmt, e), "3"); - fmt = {"%", "p", empty_no_opt}; - `check($sformatf(fmt, e), "3"); - fmt = {"%0", "p", empty_no_opt}; - `check($sformatf(fmt, e), "'h3"); - fmt = {"%0d:%", "s", ":%0d", empty_no_opt}; - `check($sformatf(fmt, 9, e, 7), "9:3:7"); - fmt = {"%", "s", " %h %", "p", empty_no_opt}; - `check($sformatf(fmt, e, 4'hA, e), "3 a 3"); - fmt = {"%", "p", empty_no_opt}; - `check($sformatf(fmt, e64), "8589934593"); - - $write("*-* All Finished *-*\n"); - $finish; - end -endmodule diff --git a/test_regress/t/t_enum_huge_methods.v b/test_regress/t/t_enum_huge_methods.v index 0d9afe845..06e64eef4 100644 --- a/test_regress/t/t_enum_huge_methods.v +++ b/test_regress/t/t_enum_huge_methods.v @@ -22,7 +22,7 @@ module t ( integer cyc = 0; my_t e; - string s; + string all; int i_cast; // Check runtime @@ -45,10 +45,6 @@ module t ( `checkh(e.prev, E01); `checkh(e.next(0), ELARGE); `checkh(e.prev(0), ELARGE); - s = $sformatf("%p", e); - `checks(s, "ELARGE"); - s = $sformatf("%s", e); // Non-standard but majority - `checks(s, "ELARGE"); e <= E01; end // @@ -72,8 +68,6 @@ module t ( end else if (cyc == 21) begin `checks(e.name, ""); // Unknown - s = $sformatf("%p", e); - `checks(s, "17"); end else if (cyc == 99) begin $write("*-* All Finished *-*\n"); diff --git a/test_regress/t/t_enum_large_methods.v b/test_regress/t/t_enum_large_methods.v index ca0882ff7..c8fbb2a16 100644 --- a/test_regress/t/t_enum_large_methods.v +++ b/test_regress/t/t_enum_large_methods.v @@ -22,7 +22,7 @@ module t ( integer cyc = 0; my_t e; - string s; + string all; // Check runtime always @(posedge clk) begin @@ -40,8 +40,6 @@ module t ( `checks(e.name, "ELARGE"); `checkh(e.next, E01); `checkh(e.prev, E01); - s = $sformatf("%p", e); - `checks(s, "ELARGE"); e <= E01; end else if (cyc == 20) begin @@ -49,8 +47,6 @@ module t ( end else if (cyc == 21) begin `checks(e.name, ""); // Unknown - s = $sformatf("%p", e); - `checks(s, "17"); end else if (cyc == 99) begin $write("*-* All Finished *-*\n"); diff --git a/test_regress/t/t_enum_type_methods.v b/test_regress/t/t_enum_type_methods.v index fd295b1ce..210c668a5 100644 --- a/test_regress/t/t_enum_type_methods.v +++ b/test_regress/t/t_enum_type_methods.v @@ -11,8 +11,8 @@ // verilog_format: on module t ( - input clk -); + input clk + ); typedef enum [3:0] { E01 = 1, @@ -20,18 +20,12 @@ module t ( E04 = 4 } my_t; - integer cyc = 0; + integer cyc = 0; my_t e; - int arrayfits[e.num]; // Check can use as constant + int arrayfits [e.num]; // Check can use as constant - typedef struct { - my_t m_a; - my_t m_b; - } mystr_t; - mystr_t mystr; - - string s; + string all; // Check constification initial begin @@ -54,38 +48,25 @@ module t ( `checkh(e.num, 3); `checks(e.name, "E03"); // - s = ""; + all = ""; for (my_t e = e.first; e != e.last; e = e.next) begin - s = {s, e.name}; + all = {all, e.name}; end e = e.last; - s = {s, e.name}; - `checks(s, "E01E03E04"); - // - e = E04; - s = $sformatf("%p", e); - `checks(s, "E04"); - s = $sformatf("%p", E03); - `checks(s, "E03"); - s = $sformatf("%s", E03); // Non-standard but majority - `checks(s, "E03"); - // - mystr.m_a = E03; - mystr.m_b = E04; - s = $sformatf("%p", mystr); - `checks(s, "'{m_a:'h3, m_b:'h4}"); + all = {all, e.name}; + `checks(all, "E01E03E04"); end localparam THREE = 3; // Check runtime - always @(posedge clk) begin + always @ (posedge clk) begin cyc <= cyc + 1; - if (cyc == 0) begin + if (cyc==0) begin // Setup e <= E01; end - else if (cyc == 1) begin + else if (cyc==1) begin `checks(e.name, "E01"); `checkh(e.next, E03); `checkh(e.next(1), E03); @@ -95,7 +76,7 @@ module t ( `checkh(e.prev(2), E03); e <= E03; end - else if (cyc == 2) begin + else if (cyc==2) begin `checks(e.name, "E03"); `checkh(e.next, E04); `checkh(e.next(1), E04); @@ -105,7 +86,7 @@ module t ( `checkh(e.prev(2), E04); e <= E04; end - else if (cyc == 3) begin + else if (cyc==3) begin `checks(e.name, "E04"); `checkh(e.next, E01); `checkh(e.next(1), E01); @@ -114,11 +95,8 @@ module t ( `checkh(e.prev(1), E03); `checkh(e.prev(2), E01); e <= E01; - // - s = $sformatf("%p", e); - `checks(s, "E04"); end - else if (cyc == 99) begin + else if (cyc==99) begin $write("*-* All Finished *-*\n"); $finish; end From ec03edcddd982a3d921cbf6fe2e7bfebd0076323 Mon Sep 17 00:00:00 2001 From: Artur Bieniek Date: Thu, 30 Apr 2026 16:38:07 +0200 Subject: [PATCH 025/350] Support functions on RHS of force (#7491) Signed-off-by: Artur Bieniek --- src/V3Task.cpp | 42 +++++++++++++++++++++++++++++++++ test_regress/t/t_force_func.out | 4 ---- test_regress/t/t_force_func.py | 2 +- test_regress/t/t_force_func.v | 5 +--- 4 files changed, 44 insertions(+), 9 deletions(-) delete mode 100644 test_regress/t/t_force_func.out diff --git a/src/V3Task.cpp b/src/V3Task.cpp index bea9cfdbd..bb0c00032 100644 --- a/src/V3Task.cpp +++ b/src/V3Task.cpp @@ -434,6 +434,7 @@ class TaskVisitor final : public VNVisitor { int m_unconVarNum = 0; // Unique bad connection variable // STATE - across all visitors + V3UniqueNames m_forceTmpNames; // For generating unique force-RHS helper names DpiCFuncs m_dpiNames; // Map of all created DPI functions VDouble0 m_statInlines; // Statistic tracking VDouble0 m_statHierDpisWithCosts; // Statistic tracking @@ -1784,6 +1785,47 @@ class TaskVisitor final : public VNVisitor { VL_DO_DANGLING(pushDeletep(nodep), nodep); } } + void visit(AstAssignForce* nodep) override { + // Force statements cannot be converted to always blocks outside of a logic block + // This causes function calls on RHS of force assignments to be improperly inlined and + // called just once. To prevent this, we create a temporary variable for each function + // reference on RHS. This variable is declared in the nearest scope and gets a continuous + // assignment of the function, so it can be converted to always and properly inlined This + // temporary variable becomes the RHS of the force assignment + std::vector refs; + nodep->rhsp()->foreach([&refs](AstNodeFTaskRef* refp) { refs.push_back(refp); }); + for (AstNodeFTaskRef* const refp : refs) { + + // Create the temporary variable and its scope + // Replicate the logic from V3Task, every function call gets + // a unique temp variable + AstVar* const interVarp = new AstVar{ + nodep->fileline(), VVarType::VAR, + refp->name() + "__Vforcefuncout" + m_forceTmpNames.get(nodep), refp->dtypep()}; + UASSERT_OBJ(m_modp->stmtsp(), m_modp, "Module should have statements in it"); + m_modp->stmtsp()->addHereThisAsNext(interVarp); + AstVarScope* const interVscp = new AstVarScope{refp->fileline(), m_scopep, interVarp}; + m_scopep->addVarsp(interVscp); + + // Recompute the helper in a combo block so any inlined function body stays + // inside schedulable logic rather than spilling statements at module scope. + AstAssign* const assignp = new AstAssign{ + nodep->fileline(), new AstVarRef{nodep->fileline(), interVscp, VAccess::WRITE}, + nodep->rhsp()->cloneTreePure(false)}; + AstSenTree* const senTreep = new AstSenTree{ + nodep->fileline(), new AstSenItem{nodep->fileline(), AstSenItem::Combo{}}}; + AstActive* const activep + = new AstActive{nodep->fileline(), "force-func-update", senTreep}; + activep->senTreeStorep(activep->sentreep()); + activep->addStmtsp( + new AstAlways{nodep->fileline(), VAlwaysKwd::ALWAYS, nullptr, assignp}); + m_scopep->addBlocksp(activep); + + // Replace RHS of force assignment with the temporary variable + refp->replaceWith(new AstVarRef{nodep->fileline(), interVscp, VAccess::READ}); + VL_DO_DANGLING(refp->deleteTree(), refp); + } + } void visit(AstNodeForeach* nodep) override { // LCOV_EXCL_LINE nodep->v3fatalSrc( "Foreach statements should have been converted to while statements in V3Begin.cpp"); diff --git a/test_regress/t/t_force_func.out b/test_regress/t/t_force_func.out deleted file mode 100644 index e5f197a92..000000000 --- a/test_regress/t/t_force_func.out +++ /dev/null @@ -1,4 +0,0 @@ -%Error: t/t_force_func.v:29: got='h0 exp='h00000001 -%Error: t/t_force_func.v:37: got='h0 exp='h00000002 -%Error: t/t_force_func.v:42: got='h0 exp='h00000003 -%Error: t/t_force_func.v:46: got='h0 exp='h00000003 diff --git a/test_regress/t/t_force_func.py b/test_regress/t/t_force_func.py index e9f4a44c9..3c6aa8922 100755 --- a/test_regress/t/t_force_func.py +++ b/test_regress/t/t_force_func.py @@ -13,6 +13,6 @@ test.scenarios('vlt') test.compile(verilator_flags2=["--binary"]) -test.execute(expect_filename=test.golden_filename) +test.execute() test.passes() diff --git a/test_regress/t/t_force_func.v b/test_regress/t/t_force_func.v index 5586d8061..5703d38e3 100644 --- a/test_regress/t/t_force_func.v +++ b/test_regress/t/t_force_func.v @@ -5,7 +5,7 @@ // SPDX-License-Identifier: CC0-1.0 // verilog_format: off -`define stop // TODO +`define stop $stop `define checkh(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0) // verilog_format: on @@ -31,9 +31,6 @@ module t; a = 2; #1; `checkh(a, 2); - // TODO - // IEEE 1800-2023 10.6 - // Assignment shall be reevaluated while the assign or force is in effect. `checkh(b, 2); a = 3; From 25d4827bd506ecf5cb43545f97e052472a4e65e2 Mon Sep 17 00:00:00 2001 From: Igor Zaworski Date: Thu, 30 Apr 2026 22:56:15 +0200 Subject: [PATCH 026/350] Internals: Four state pre-pull (types) (#7520) --- bin/verilator | 2 + docs/guide/exe_verilator.rst | 8 ++ src/V3Ast.cpp | 2 +- src/V3Ast.h | 9 +- src/V3AstNodeExpr.h | 112 ++++++++++++++---- src/V3Begin.cpp | 6 +- src/V3EmitCImp.cpp | 2 + src/V3LinkJump.cpp | 2 +- src/V3Number.cpp | 10 +- src/V3Options.cpp | 4 + src/V3Randomize.cpp | 12 +- src/V3Width.cpp | 54 ++++----- test_regress/t/t_debug_emitv.out | 4 +- test_regress/t/t_eq_wild.v | 19 ++- .../t/t_fourstate_fourstate_hier_unsup.out | 6 + .../t/t_fourstate_fourstate_hier_unsup.py | 20 ++++ test_regress/t/t_json_only_flat_vlvbound.out | 2 +- test_regress/t/t_mod_interface_array4.v | 2 +- 18 files changed, 196 insertions(+), 80 deletions(-) create mode 100644 test_regress/t/t_fourstate_fourstate_hier_unsup.out create mode 100755 test_regress/t/t_fourstate_fourstate_hier_unsup.py diff --git a/bin/verilator b/bin/verilator index 5fefa73bd..22bb364ee 100755 --- a/bin/verilator +++ b/bin/verilator @@ -416,6 +416,8 @@ detailed descriptions of these arguments. -f Parse arguments from a file -FI Force include of a file --flatten Force inlining of all modules, tasks and functions + --fourstate Enable fourstate logic + --no-fourstate Disable fourstate logic --func-recursion-depth Maximum recursive constant function depth --future0