From 048d18a84f47322a7b6a2a233d0e6007e19715a1 Mon Sep 17 00:00:00 2001 From: Matthew Ballance Date: Mon, 7 Sep 2026 11:02:03 -0700 Subject: [PATCH] Add covergroup runtime registry (#8134) --- include/verilated.h | 6 + include/verilated_covergroup.cpp | 186 +++++++++- include/verilated_covergroup.h | 229 +++++++++++- src/V3AstAttr.h | 29 +- src/V3AstNodeDType.h | 34 ++ src/V3AstNodes.cpp | 13 + src/V3Clean.cpp | 1 + src/V3Covergroup.cpp | 331 +++++++++++------- src/V3EmitCFunc.cpp | 6 + src/V3EmitCFunc.h | 6 + test_regress/t/t_covergroup_inst_churn.py | 25 ++ test_regress/t/t_covergroup_inst_churn.v | 102 ++++++ test_regress/t/t_covergroup_inst_handle.cpp | 101 ++++++ test_regress/t/t_covergroup_inst_handle.py | 44 +++ test_regress/t/t_covergroup_inst_handle.v | 43 +++ test_regress/t/t_covergroup_inst_lifetime.out | 4 + test_regress/t/t_covergroup_inst_lifetime.py | 15 + test_regress/t/t_covergroup_inst_lifetime.v | 56 +++ test_regress/t/t_covergroup_inst_retain.out | 4 + test_regress/t/t_covergroup_inst_retain.py | 29 ++ test_regress/t/t_covergroup_inst_retain.v | 124 +++++++ test_regress/t/t_covergroup_inst_retire.py | 21 ++ test_regress/t/t_covergroup_inst_retire.v | 275 +++++++++++++++ .../t/t_covergroup_inst_retire_asan.py | 30 ++ test_regress/t/t_covergroup_multi_inst.out | 6 + test_regress/t/t_covergroup_multi_inst.py | 15 + test_regress/t/t_covergroup_multi_inst.v | 67 ++++ 27 files changed, 1661 insertions(+), 141 deletions(-) create mode 100755 test_regress/t/t_covergroup_inst_churn.py create mode 100644 test_regress/t/t_covergroup_inst_churn.v create mode 100644 test_regress/t/t_covergroup_inst_handle.cpp create mode 100755 test_regress/t/t_covergroup_inst_handle.py create mode 100644 test_regress/t/t_covergroup_inst_handle.v create mode 100644 test_regress/t/t_covergroup_inst_lifetime.out create mode 100755 test_regress/t/t_covergroup_inst_lifetime.py create mode 100644 test_regress/t/t_covergroup_inst_lifetime.v create mode 100644 test_regress/t/t_covergroup_inst_retain.out create mode 100755 test_regress/t/t_covergroup_inst_retain.py create mode 100644 test_regress/t/t_covergroup_inst_retain.v create mode 100755 test_regress/t/t_covergroup_inst_retire.py create mode 100644 test_regress/t/t_covergroup_inst_retire.v create mode 100755 test_regress/t/t_covergroup_inst_retire_asan.py create mode 100644 test_regress/t/t_covergroup_multi_inst.out create mode 100755 test_regress/t/t_covergroup_multi_inst.py create mode 100644 test_regress/t/t_covergroup_multi_inst.v diff --git a/include/verilated.h b/include/verilated.h index 64a04a233..050c7601c 100644 --- a/include/verilated.h +++ b/include/verilated.h @@ -109,6 +109,7 @@ class VerilatedVarNameMap; class VerilatedVcd; class VerilatedVcdC; class VerilatedVcdSc; +class VlCovRegistry; //========================================================================= // Basic types @@ -590,6 +591,9 @@ protected: std::unique_ptr m_executionProfiler; // Coverage access std::unique_ptr m_coveragep; // Pointer for coveragep() + // Covergroup type/instance nodes. Covergroup data is always collected, + // independent of whether coverage data is recorded (--coverage). + std::unique_ptr m_covergroupsp; // Pointer for covergroupRegistryp() // File I/O // Not serialized @@ -659,6 +663,8 @@ public: /// Return VerilatedCovContext, allocate if needed /// Note if get unresolved reference then likely forgot to link verilated_cov.cpp VerilatedCovContext* coveragep() VL_MT_SAFE; + /// Returns VlCovRegistry. Allocated on-demand + VlCovRegistry* covergroupRegistryp() VL_MT_SAFE; /// Return debug level static inline int debug() VL_MT_SAFE; /// Set debug level /// Debug is currently global, but for forward compatibility have a per-context method diff --git a/include/verilated_covergroup.cpp b/include/verilated_covergroup.cpp index 3011fce77..76b91ac00 100644 --- a/include/verilated_covergroup.cpp +++ b/include/verilated_covergroup.cpp @@ -27,12 +27,13 @@ // This file is compiled whenever covergroups are used, with or without // "verilator --coverage" (see V3Global::verilatedCppFiles). Bin counts are -// members of the covergroup objects themselves, so sampling, bin naming, and -// coverage queries such as get_inst_coverage() all work with no coverage -// database present. VL_COVER_INSERT does not copy a count; it hands the -// database the address of a counter to read at write time. Only that -// publication step needs verilated_cov.cpp, which is compiled solely under -// --coverage, so only the registerBins() bodies are gated on VM_COVERAGE. +// owned by the covergroup instance nodes in the VerilatedContext's registry, so +// sampling, bin naming, and coverage queries such as get_inst_coverage() all +// work with no coverage database present. VL_COVER_INSERT does not copy a +// count; it hands the database the address of a counter the registry owns and +// reads it at write time. Only that publication step needs the database, so +// only the registerBins() bodies -- and this include -- are gated on +// VM_COVERAGE. #if VM_COVERAGE #include "verilated_cov.h" #endif @@ -155,9 +156,10 @@ void VlCoverCross::finalizeBins() { } } -void VlCoverCross::iterateProduct(VlCoverpoint* const* cps, uint32_t dim, uint32_t baseIdx) { - const uint32_t hits = cps[dim]->hitCount(); - const uint32_t* const list = cps[dim]->hitList(); +void VlCoverCross::iterateProduct(uint32_t dim, uint32_t baseIdx) { + const VlCoverpoint* const cpp = m_cps[dim]; + const uint32_t hits = cpp->hitCount(); + const uint32_t* const list = cpp->hitList(); const bool last = (dim == m_dims - 1); const uint32_t stride = m_stride[dim]; for (uint32_t hit = 0; hit < hits; ++hit) { @@ -165,19 +167,19 @@ void VlCoverCross::iterateProduct(VlCoverpoint* const* cps, uint32_t dim, uint32 if (last) { incrementTuple(idx); } else { - iterateProduct(cps, dim + 1, idx); + iterateProduct(dim + 1, idx); } } } -void VlCoverCross::sample(VlCoverpoint* const* cps, const bool* binIffs) { +void VlCoverCross::sample(const bool* binIffs) { // Fast path: if any dimension had no Normal-bin hit, the cross cannot hit. for (uint32_t d = 0; d < m_dims; ++d) { - if (cps[d]->hitCount() == 0) return; + if (m_cps[d]->hitCount() == 0) return; } for (Bin& bin : m_bins) { if (binIffs && !*binIffs++) continue; - const VlCoverpoint* const cpp = cps[bin.dim]; + const VlCoverpoint* const cpp = m_cps[bin.dim]; for (uint32_t hit = 0; hit < cpp->hitCount(); ++hit) { const uint32_t idx = cpp->hitList()[hit]; if (idx >= bin.first && idx - bin.first < bin.bins) { @@ -186,7 +188,7 @@ void VlCoverCross::sample(VlCoverpoint* const* cps, const bool* binIffs) { } } } - iterateProduct(cps, 0, 0); + iterateProduct(0, 0); } std::string VlCoverCross::binName(uint32_t i) const { @@ -237,3 +239,159 @@ void VlCoverCross::registerBins(VerilatedCovContext* covcontextp, const char* pa } } #endif // VM_COVERAGE + +//============================================================================= +// VlCovergroupType / VlCovRegistry + +VlCovergroupInst* VlCovergroupType::newInstance() { + VlCovergroupInst* const instp = new VlCovergroupInst{this, m_nextInstId++}; + m_insts.emplace_back(instp); +#if !VM_COVERAGE + instp->m_slot = static_cast(m_insts.size() - 1); +#endif + ++m_createdInsts; + return instp; +} + +void VlCovergroupType::foldResidue(const VlCovergroupInst* instp) { + double covered = 0.0; + double total = 0.0; + instp->coverageParts(covered, total); + // Nothing coverable: excluded from both sums, so it moves neither the mean + // nor the denominator. Never-sampled is different: it has bins, none hit, + // and folds as 0%. + if (total == 0.0) return; + // TODO(P5): IEEE 1800-2023 19.5 defines covergroup coverage as the weighted + // mean of the per-item ratios, not the ratio of the summed parts. This + // matches what the generated get_inst_coverage() computes today, so that a + // live instance and the same instance one delta after death never disagree. + m_retired.sumCoverage += 100.0 * covered / total; + ++m_retired.count; +} + +// Runs when the last handle to instp drops, possibly after ~VlCovRegistry, on a +// type teardown leaked to keep this valid (see ~VlCovRegistry). That late case +// needs no special handling: the leaked type is self-consistent. +void VlCovergroupType::retire(VlCovergroupInst* instp) { + foldResidue(instp); // Before unlink: reads instp's items, freed below + +#if VM_COVERAGE + // registerBins() gave the coverage database raw &m_counts[i], read at + // write() time. Keep the node alive, marked dead so it counts as neither + // live nor residue. Freeing here needs the coverage-writer rework. + instp->m_retained = true; +#else + // Move out first, so the node destructs at end of scope with m_insts + // already consistent rather than mid-swap. + const uint32_t slot = instp->m_slot; + const std::unique_ptr dying = std::move(m_insts[slot]); + if (slot != m_insts.size() - 1) { + m_insts[slot] = std::move(m_insts.back()); + m_insts[slot]->m_slot = slot; // Moved node's slot is now stale + } + m_insts.pop_back(); +#endif +} + +uint32_t VlCovergroupType::liveInstanceCount() const { + uint32_t live = 0; + // Under VM_COVERAGE m_insts also holds retained (dead) nodes; otherwise + // retained() is never set and this equals m_insts.size(). + for (const auto& instp : m_insts) { + if (!instp->retained()) ++live; + } + return live; +} + +bool VlCovergroupType::anyAttached() const { + for (const auto& instp : m_insts) { + if (instp->m_attachCount > 0) return true; + } + return false; +} + +double VlCovergroupType::retiredCoverage() const { + if (m_retired.count == 0) return -1.0; + return m_retired.sumCoverage / static_cast(m_retired.count); +} + +// Defined here, not in verilated.cpp, so that the registry costs nothing in a model with no +// covergroups: this file is linked only when covergroups are used (or --coverage is on). +// Mirrors VerilatedContext::coveragep(), which lives in verilated_cov.cpp for the same reason. +VlCovRegistry* VerilatedContext::covergroupRegistryp() VL_MT_SAFE { + static VerilatedMutex s_mutex; + // cppcheck-suppress identicalInnerCondition + if (VL_UNLIKELY(!m_covergroupsp)) { + const VerilatedLockGuard lock{s_mutex}; + // cppcheck-suppress identicalInnerCondition + if (VL_LIKELY(!m_covergroupsp)) { // LCOV_EXCL_LINE // Not redundant, prevents race + m_covergroupsp.reset(new VlCovRegistry{}); + } + } + return static_cast(m_covergroupsp.get()); +} + +VlCovergroupInst* VlCovRegistry::newCovergroupInst(const char* typeName) { + VlCovergroupType*& typep = m_byName[typeName]; + if (!typep) { // First instance of this type + m_types.emplace_back(new VlCovergroupType{}); + typep = m_types.back().get(); + } + return typep->newInstance(); +} + +// A covergroup object can outlive the registry: models must be destroyed before +// their context, and a user who gets that backwards drops covergroup handles +// after ~VerilatedContext. Those handle destructors call attachDec(), which +// reads the instance node and its type -- so freeing the nodes here is itself +// what would make the wrong ordering a use-after-free, and a "retirement +// disarmed" flag could not help. Instead, leak any type that still has an +// attached node, keeping the type, its nodes and their items valid; the late +// retire() then frees the nodes itself, so only the type object leaks. +VlCovRegistry::~VlCovRegistry() { + for (auto& typep : m_types) { + // Normally nothing is still attached; if something is, the model + // outlived its context and those handles still reach this type. + if (VL_UNLIKELY(typep->anyAttached())) { + VlCovergroupType* const leakedp = typep.release(); + static_cast(leakedp); // Deliberate leak + } + } +} + +VlCovergroupType* VlCovRegistry::findType(const char* typeName) const { + const auto it = m_byName.find(typeName); + return it == m_byName.end() ? nullptr : it->second; +} + +uint32_t VlCovRegistry::liveInstanceCount() const { + uint32_t total = 0; + for (const auto& typep : m_types) total += typep->liveInstanceCount(); + return total; +} + +uint32_t VlCovRegistry::createdInstanceCount() const { + uint32_t total = 0; + for (const auto& typep : m_types) total += typep->createdInstanceCount(); + return total; +} + +uint32_t VlCovRegistry::liveInstanceCount(const char* typeName) const { + const VlCovergroupType* const typep = findType(typeName); + return typep ? typep->liveInstanceCount() : 0; +} + +uint32_t VlCovRegistry::createdInstanceCount(const char* typeName) const { + const VlCovergroupType* const typep = findType(typeName); + return typep ? typep->createdInstanceCount() : 0; +} + +uint32_t VlCovRegistry::retiredInstanceCount(const char* typeName) const { + const VlCovergroupType* const typep = findType(typeName); + return typep ? typep->retiredInstanceCount() : 0; +} + +double VlCovRegistry::retiredCoverage(const char* typeName) const { + const VlCovergroupType* const typep = findType(typeName); + return typep ? typep->retiredCoverage() : -1.0; +} diff --git a/include/verilated_covergroup.h b/include/verilated_covergroup.h index ec0211a95..b9b68b2d0 100644 --- a/include/verilated_covergroup.h +++ b/include/verilated_covergroup.h @@ -32,10 +32,13 @@ #include "verilatedos.h" +#include "verilated.h" #include "verilated_cov_model.h" #include +#include #include +#include #include class VerilatedCovContext; @@ -245,14 +248,14 @@ class VlCoverCross final : public VlCoverpointIf { std::vector m_cpBinCounts; // [m_dims] Normal bin count per dimension std::vector m_stride; // [m_dims] Flat-index stride per dimension std::vector m_flatCounts; // [m_numAutoBins] Per-bin hit counts - std::vector m_cps; // Feeding coverpoints (automatic-bin name source) + std::vector m_cps; // Feeding coverpoints, set by init() std::vector m_bins; // Explicit bins in declaration order std::vector m_autoExcluded; // Tuples replaced by explicit bins; empty for auto-only crosses std::vector m_autoBins; // Retained flat indices, when explicit bins are present // PRIVATE METHODS - void iterateProduct(VlCoverpoint* const* cps, uint32_t dim, uint32_t baseIdx); + void iterateProduct(uint32_t dim, uint32_t baseIdx); void incrementTuple(uint32_t idx) { if (!m_autoExcluded.empty() && m_autoExcluded[idx]) return; if (m_flatCounts[idx]++ == 0) ++m_numCovered; @@ -277,7 +280,8 @@ public: // ---- hot path (from generated sample(), after all coverpoints sampled) ---- /// Sample automatic and explicit bins, optionally applying per-bin iff guards. - void sample(VlCoverpoint* const* cps, const bool* binIffs = nullptr); + /// Reads the feeding coverpoints from m_cps, so the caller passes no coverpoints. + void sample(const bool* binIffs = nullptr); // ---- VlCoverpointIf ---- // Explicit bins precede retained automatic bins; all are Normal bins. @@ -292,4 +296,223 @@ public: } }; +class VlCovergroupType; + +//============================================================================= +// VlCovergroupInst +/// One covergroup instance: owns the coverpoint/cross runtimes created by one +/// SV 'new'. The generated class holds borrowed pointers to them, so the bins +/// outlive the SV object -- the coverage database registers raw count pointers +/// and reads them at write() time, long after the object may have been freed. +/// +/// Attach-counted: every VlCovInstHandle bound here holds one count, and the +/// node is retired (see VlCovergroupType::retire) when the last one drops. + +class VlCovergroupInst final { + // MEMBERS + // Coverpoint and cross runtimes of this instance; creation == declaration order + std::vector> m_items; + VlCovergroupType* const m_typep; // Owning type; outlives this node + const uint32_t m_instId; // Stable identity across churn; NOT the slot +#if !VM_COVERAGE + // Only retire()'s free path uses this; under VM_COVERAGE the node is never + // unlinked, so the slot would be dead. VlCovergroupType sets it. + uint32_t m_slot = 0; // Index into m_typep->m_insts; unlink-by-swap rewrites +#endif + uint32_t m_attachCount = 1; // SV handles bound here; 1 from construction + bool m_retained = false; // VM_COVERAGE: dead, but kept for registered count pointers + + // Reads m_items to fold the residue; owns m_slot and m_retained. + friend class VlCovergroupType; + +public: + // CONSTRUCTORS + VlCovergroupInst(VlCovergroupType* typep, uint32_t instId) + : m_typep{typep} + , m_instId{instId} {} + VL_UNCOPYABLE(VlCovergroupInst); + + // METHODS + // ---- construction (from the generated covergroup constructor) ---- + template + VlCoverpointT* addCoverpoint() { + VlCoverpointT* const cpp = new VlCoverpointT{}; + m_items.emplace_back(cpp); + return cpp; // borrowed by the generated class + } + VlCoverCross* addCross() { + VlCoverCross* const cxp = new VlCoverCross{}; + m_items.emplace_back(cxp); + return cxp; // borrowed by the generated class + } + + // ---- attach counting (from VlCovInstHandle) ---- + void attachInc() { ++m_attachCount; } + // Drops one handle; true if it was the last and the caller must retire the + // node. Retiring is the caller's job because VlCovergroupType is incomplete + // here, and because it frees 'this'. + bool attachDec() { return --m_attachCount == 0; } + + // ---- introspection ---- + VlCovergroupType* typep() const { return m_typep; } + uint32_t instId() const { return m_instId; } + // True once retired but kept alive because the coverage database holds raw + // pointers into this node's bin counts (VM_COVERAGE); see retire(). + bool retained() const { return m_retained; } + // Sum of the instance's items' covered/total bin counts. Matches what the + // generated get_inst_coverage() computes; see foldResidue(). + void coverageParts(double& covered, double& total) const { + covered = 0.0; + total = 0.0; + for (const auto& itemp : m_items) { + double c = 0.0; + double t = 0.0; + itemp->coverageParts(c, t); + covered += c; + total += t; + } + } +}; + +//============================================================================= +// VlCovRetiredAvg +/// Per-type residue: what survives an instance's death. Fixed size, so it does +/// not grow with churn. Weight is 1 everywhere until option.weight is plumbed. + +struct VlCovRetiredAvg final { + uint64_t count = 0; // Retired instances that contributed (nonzero denominator) + double sumCoverage = 0.0; // Sigma of per-instance coverage, each in 0..100 +}; + +//============================================================================= +// VlCovergroupType +/// One covergroup type: owns its live instances, in creation order, plus the +/// residue of the ones that have died. + +class VlCovergroupType final { + // MEMBERS + // Live nodes, and -- under VM_COVERAGE -- retired-but-retained ones. Slot + // order is creation order only until the first unlink-by-swap. + std::vector> m_insts; + uint32_t m_createdInsts = 0; // Instances ever created; never decremented + uint32_t m_nextInstId = 0; // Monotonic; slots are reused, ids never are + VlCovRetiredAvg m_retired; // Contribution of every instance that has died + + // PRIVATE METHODS + // Harvest instp's contribution into m_retired. Must run before instp is + // unlinked: it reads the instance's items. + void foldResidue(const VlCovergroupInst* instp); + +public: + // CONSTRUCTORS + VlCovergroupType() = default; + VL_UNCOPYABLE(VlCovergroupType); + + // METHODS + VlCovergroupInst* newInstance(); + // Called when the last handle to instp drops. Folds the residue, then + // unlinks and frees the node -- except under VM_COVERAGE, where the coverage + // database still holds raw pointers into it and it is only marked retained. + void retire(VlCovergroupInst* instp); + // True if any node here still has an SV handle bound to it, and so can be + // retired again after the registry is destroyed. See ~VlCovRegistry. + bool anyAttached() const; + + // ---- introspection ---- + // Test and debug only; generated code never calls these, and SV reaches them + // only via explicit $c. They let a regression test pin node accumulation + // (otherwise visible only as memory growth) and the residue fold. + // + // Instance nodes still reachable from SV. Under VM_COVERAGE this is smaller + // than m_insts.size(), which also holds retained (dead) nodes. + uint32_t liveInstanceCount() const; + // Instances ever created, live or not. Wraps after 4G instances, which no + // introspection use cares about. + uint32_t createdInstanceCount() const { return m_createdInsts; } + // Instances that have died and contributed to the residue. + uint32_t retiredInstanceCount() const { return static_cast(m_retired.count); } + // Mean coverage over the retired instances only, in 0..100; -1.0 if none. + double retiredCoverage() const; +}; + +//============================================================================= +// VlCovRegistry +/// Every covergroup type and instance in one VerilatedContext. Owned by the +/// VerilatedContext (not by the coverage database, which is only linked under +/// --coverage and is a *consumer* of this data), reached through +/// VerilatedContext::covergroupRegistryp(). + +class VlCovRegistry final : public VerilatedVirtualBase { + // MEMBERS + std::vector> m_types; // Creation order + std::unordered_map m_byName; // Lookup, borrowed + + // PRIVATE METHODS + VlCovergroupType* findType(const char* typeName) const; // nullptr if unknown + +public: + // CONSTRUCTORS + VlCovRegistry() = default; + ~VlCovRegistry() override; + VL_UNCOPYABLE(VlCovRegistry); + + // METHODS + // Find-or-create the type node, then add an instance to it. typeName is the + // generated covergroup class name, already --protect-ids obfuscated, and is + // the same string that keys the coverage database's hier/page. + VlCovergroupInst* newCovergroupInst(const char* typeName); + + // ---- introspection (see VlCovergroupType) ---- + // typeName is the obfuscated generated name, so a test using these under + // --protect-ids must pass the obfuscated string; the no-argument form does not. + uint32_t liveInstanceCount() const; // Summed over every type + uint32_t createdInstanceCount() const; // Summed over every type + uint32_t liveInstanceCount(const char* typeName) const; // 0 if type unknown + uint32_t createdInstanceCount(const char* typeName) const; // 0 if type unknown + uint32_t retiredInstanceCount(const char* typeName) const; // 0 if type unknown + double retiredCoverage(const char* typeName) const; // -1.0 if type unknown or none +}; + +//============================================================================= +// VlCovInstHandle +/// The generated covergroup class's link to its instance node. Attach-counting: +/// the registry owns the node, but the handles are what keep it reachable, and +/// the last one to go retires it. +/// +/// Must stay copyable: every generated clone() copy-constructs. A copy shares +/// the node, and so the bin counts -- pre-existing covergroup-copy aliasing. +/// Attach counting makes that lifetime-safe, not correct. + +class VlCovInstHandle final { + // MEMBERS + VlCovergroupInst* m_p = nullptr; // Attach-counted; the registry owns the node + + // PRIVATE METHODS + // Drop one attach count, retiring the node if that was the last handle. + // Nothing may touch instp afterwards: retire() may have freed it. + static void release(VlCovergroupInst* instp) { + if (VL_UNCOVERABLE(!instp)) return; // Never attach()ed; codegen always does + if (instp->attachDec()) instp->typep()->retire(instp); + } + +public: + // CONSTRUCTORS + VlCovInstHandle() = default; + VlCovInstHandle(const VlCovInstHandle& o) + : m_p{o.m_p} { + if (VL_UNCOVERABLE(!m_p)) return; // Unbound source; see release above + m_p->attachInc(); + } + // Deleted, not implemented: nothing generates an assignment, and the + // implicit one would copy m_p raw -- no attachInc, no release. + VlCovInstHandle& operator=(const VlCovInstHandle&) = delete; + ~VlCovInstHandle() { release(m_p); } + + // METHODS + // Bind to a freshly created node, taking over the attach count of 1 it was + // created with. Called once, from the generated covergroup constructor. + void attach(VlCovergroupInst* p) { m_p = p; } + VlCovergroupInst* p() const { return m_p; } +}; + #endif // Guard diff --git a/src/V3AstAttr.h b/src/V3AstAttr.h index 3232816b3..574b8db57 100644 --- a/src/V3AstAttr.h +++ b/src/V3AstAttr.h @@ -509,6 +509,8 @@ public: PROCESS_REFERENCE, RANDOM_GENERATOR, RANDOM_STDGENERATOR, + COVERGROUP_INSTHANDLE, + COVERGROUP_CROSS, // Unsigned and two state; fundamental types UINT32, UINT64, @@ -544,6 +546,8 @@ public: "VlProcessRef", "VlRandomizer", "VlStdRandomizer", + "VlCovInstHandle", + "VlCoverCross*", "IData", "QData", "LOGIC_IMPLICIT", @@ -576,6 +580,8 @@ public: "%E-proc-ref", "%E-rand-gen", "%E-stdrand-gen", + "%E-cg-insthandle", + "%E-cover-cross", "IData", "QData", "%E-logic-implct", @@ -620,6 +626,8 @@ public: case PROCESS_REFERENCE: return 0; // opaque case RANDOM_GENERATOR: return 0; // opaque case RANDOM_STDGENERATOR: return 0; // opaque + case COVERGROUP_INSTHANDLE: return 0; // opaque + case COVERGROUP_CROSS: return 0; // opaque case UINT32: return 32; case UINT64: return 64; default: return 0; @@ -660,7 +668,8 @@ public: return (m_e == EVENT || m_e == STRING || m_e == SCOPEPTR || m_e == CHARPTR || m_e == MTASKSTATE || m_e == DELAY_SCHEDULER || m_e == TRIGGER_SCHEDULER || m_e == DYNAMIC_TRIGGER_SCHEDULER || m_e == FORK_SYNC || m_e == PROCESS_REFERENCE - || m_e == RANDOM_GENERATOR || m_e == RANDOM_STDGENERATOR || m_e == DOUBLE + || m_e == RANDOM_GENERATOR || m_e == RANDOM_STDGENERATOR + || m_e == COVERGROUP_INSTHANDLE || m_e == COVERGROUP_CROSS || m_e == DOUBLE || m_e == UNTYPED); } bool isCHandle() const VL_MT_SAFE { return m_e == CHANDLE; } @@ -716,6 +725,8 @@ public: /* PROCESS_REFERENCE: */ "", // Should not be traced /* RANDOM_GENERATOR: */ "", // Should not be traced /* RANDOM_STD_GENERATOR: */ "", // Should not be traced + /* COVERGROUP_INSTHANDLE: */ "", // Should not be traced + /* COVERGROUP_CROSS: */ "", // Should not be traced /* UINT32: */ "BIT", /* UINT64: */ "BIT", /* LOGIC_IMPLICIT: */ "", // Should not be traced @@ -865,6 +876,22 @@ inline std::ostream& operator<<(std::ostream& os, const VBranchPred& rhs) { macro(ASSOC_NEXT, "next", false, "m") \ macro(ASSOC_SIZE, "size", PURE, "") \ macro(CLASS_SET_RANDMODE, "set_randmode", false, "r") \ + macro(COVERGROUP_ADD_ARRAY_NAMER, "addArrayNamer", false, "r+") \ + macro(COVERGROUP_ADD_BIN, "addBin", false, "r+") \ + macro(COVERGROUP_ADD_COVERPOINT, "addCoverpoint", false, "") \ + macro(COVERGROUP_ADD_CROSS, "addCross", false, "") \ + macro(COVERGROUP_ADD_SINGLE_NAMER, "addSingleNamer", false, "r+") \ + macro(COVERGROUP_ATTACH, "attach", false, "r") \ + macro(COVERGROUP_CLEAR_HIT_LIST, "clearHitList", false, "") \ + macro(COVERGROUP_COVERAGE_PARTS, "coverageParts", false, "TODO") \ + macro(COVERGROUP_FINALIZE_BINS, "finalizeBins", false, "") \ + macro(COVERGROUP_INCREMENT_BIN, "incrementBin", false, "r") \ + macro(COVERGROUP_INIT, "init", false, "r+") \ + macro(COVERGROUP_INST_P, "p", PURE, "") \ + macro(COVERGROUP_RECORD_HIT, "recordHit", false, "r") \ + macro(COVERGROUP_REGISTER_BINS, "registerBins", false, "rr") \ + macro(COVERGROUP_SAMPLE, "sample", false, "") \ + macro(COVERGROUP_SAMPLE_IFFS, "sample", false, "r") \ macro(DYN_AT_WRITE_APPEND, "atWriteAppend", false, "r") \ macro(DYN_AT_WRITE_APPEND_BACK, "atWriteAppendBack", false, "r") \ macro(DYN_CLEAR, "clear", false, "") \ diff --git a/src/V3AstNodeDType.h b/src/V3AstNodeDType.h index 11b701c36..4cd019497 100644 --- a/src/V3AstNodeDType.h +++ b/src/V3AstNodeDType.h @@ -498,6 +498,12 @@ public: bool isStdRandomGenerator() const VL_MT_SAFE { return keyword() == VBasicDTypeKwd::RANDOM_STDGENERATOR; } + bool isCovergroupInstHandle() const VL_MT_SAFE { + return keyword() == VBasicDTypeKwd::COVERGROUP_INSTHANDLE; + } + bool isCovergroupCross() const VL_MT_SAFE { + return keyword() == VBasicDTypeKwd::COVERGROUP_CROSS; + } bool isOpaque() const VL_MT_SAFE { return keyword().isOpaque(); } bool isString() const VL_MT_STABLE { return keyword().isString(); } bool isZeroInit() const { return keyword().isZeroInit(); } @@ -687,6 +693,34 @@ public: int widthTotalBytes() const override { return 1; } bool isCompound() const override { return false; } }; +class AstCoverpointDType final : public AstNodeDType { + // Borrowed pointer to a covergroup coverpoint runtime, 'VlCoverpointT*'. + // Follows pattern of AstQueueDType in capturing the compile-time max bin overlap + // template argument. + uint32_t m_hitBound; // VlCoverpointT<> template argument; hit list size, >= 1 +public: + AstCoverpointDType(FileLine* fl, uint32_t hitBound) + : ASTGEN_SUPER_CoverpointDType(fl) + , m_hitBound{hitBound} { + dtypep(this); + } + ASTGEN_MEMBERS_AstCoverpointDType; + const char* broken() const override { + BROKEN_RTN(m_hitBound < 1); + return nullptr; + } + // V3Covergroup interns these one-per-hitBound into the type table, so identity is + // equality; there is never a second node with the same bound to compare against. + bool similarDTypeNode(const AstNodeDType* samep) const override { return this == samep; } + void dumpSmall(std::ostream& str) const override; + // ACCESSORS + uint32_t hitBound() const { return m_hitBound; } + // METHODS + AstBasicDType* basicp() const override VL_MT_STABLE { return nullptr; } + int widthAlignBytes() const override { return sizeof(void*); } + int widthTotalBytes() const override { return sizeof(void*); } + bool isCompound() const override { return true; } +}; class AstDefImplicitDType final : public AstNodeDType { // For parsing enum/struct/unions that are declared with a variable rather than typedef // This allows "var enum {...} a,b" to share the enum definition for both variables diff --git a/src/V3AstNodes.cpp b/src/V3AstNodes.cpp index 37f573b2c..dc4d76418 100644 --- a/src/V3AstNodes.cpp +++ b/src/V3AstNodes.cpp @@ -1270,6 +1270,10 @@ void AstCoverTransSet::dumpJson(std::ostream& str) const { Super::dumpJson(str); // Functional coverage dump methods void AstCoverpoint::dump(std::ostream& str) const { Super::dump(str); } void AstCoverpoint::dumpJson(std::ostream& str) const { Super::dumpJson(str); } +void AstCoverpointDType::dumpSmall(std::ostream& str) const { + Super::dumpSmall(str); + str << "coverpoint[" << m_hitBound << "]"; +} void AstCoverpointRef::dump(std::ostream& str) const { Super::dump(str); } void AstCoverpointRef::dumpJson(std::ostream& str) const { Super::dumpJson(str); } void AstCvtArrayToArray::dump(std::ostream& str) const { @@ -2132,6 +2136,9 @@ AstNodeDType::CTypeRecursed AstNodeDType::cTypeRecurse(bool compound, bool packe // + 1 below as VlQueue uses 0 to mean unlimited, 1 to mean size() max is 1 if (adtypep->boundp()) info.m_type += ", " + cvtToStr(adtypep->boundConst() + 1); info.m_type += ">"; + } else if (const auto* const adtypep = VN_CAST(dtypep, CoverpointDType)) { + UASSERT_OBJ(!packed, this, "Unsupported type for packed struct or union"); + info.m_type = "VlCoverpointT<" + cvtToStr(adtypep->hitBound()) + ">*"; } else if (const auto* const adtypep = VN_CAST(dtypep, SampleQueueDType)) { UASSERT_OBJ(!packed, this, "Unsupported type for packed struct or union"); const CTypeRecursed sub = adtypep->subDTypep()->cTypeRecurse(true, false); @@ -2207,6 +2214,12 @@ AstNodeDType::CTypeRecursed AstNodeDType::cTypeRecurse(bool compound, bool packe info.m_type = "VlRandomizer"; } else if (bdtypep->isStdRandomGenerator()) { info.m_type = "VlStdRandomizer"; + } else if (bdtypep->isCovergroupInstHandle()) { + info.m_type = "VlCovInstHandle"; + } else if (bdtypep->isCovergroupCross()) { + // Borrowed pointer: VlCovergroupInst owns the cross runtime, so its bins outlive the + // SV covergroup object (the coverage DB holds raw count pointers read at write() time) + info.m_type = "VlCoverCross*"; } else if (bdtypep->isEvent()) { info.m_type = v3Global.assignsEvents() ? "VlAssignableEvent" : "VlEvent"; } else if (dtypep->widthMin() <= 8) { // Handle unpacked arrays; not bdtypep->width diff --git a/src/V3Clean.cpp b/src/V3Clean.cpp index 8366b23b9..3c015bb0b 100644 --- a/src/V3Clean.cpp +++ b/src/V3Clean.cpp @@ -91,6 +91,7 @@ class CleanVisitor final : public VNVisitor { || VN_IS(nodep->dtypep()->skipRefp(), QueueDType) || VN_IS(nodep->dtypep()->skipRefp(), StreamDType) || VN_IS(nodep->dtypep()->skipRefp(), UnpackArrayDType) + || VN_IS(nodep->dtypep()->skipRefp(), CoverpointDType) || VN_IS(nodep->dtypep()->skipRefp(), VoidDType)) { } else { const AstNodeUOrStructDType* const dtypep diff --git a/src/V3Covergroup.cpp b/src/V3Covergroup.cpp index c04b3eb78..664636c10 100644 --- a/src/V3Covergroup.cpp +++ b/src/V3Covergroup.cpp @@ -170,8 +170,8 @@ class FunctionalCoverageVisitor final : public VNVisitor { std::map m_cpBins; // Runtime coverpoint -> binsof index ranges std::set m_droppedCrosses; // Crosses with a bare-variable item: drop (COVERIGN) - std::map m_vlCoverpointTypes; // hit-list bound K -> "VlCoverpointT" type - AstCDType* m_vlCoverCrossDTypep = nullptr; // Shared "VlCoverCross" C++ member type + std::map m_cpDTypes; // Hit-list bound -> interned dtype + AstVar* m_cgInstVarp = nullptr; // __Vcg_inst handle member of the current covergroup VMemberMap m_memberMap; // Member names cached for fast lookup @@ -187,6 +187,7 @@ class FunctionalCoverageVisitor final : public VNVisitor { m_cpVarMap.clear(); m_cpBins.clear(); m_droppedCrosses.clear(); + m_cgInstVarp = nullptr; // Scan every cross item to record the coverpoints it references (the cross dimensions) // and to flag any cross naming a bare variable -- a would-be implicit coverpoint, which @@ -204,6 +205,10 @@ class FunctionalCoverageVisitor final : public VNVisitor { } } + // The instance node owns this instance's coverpoint/cross runtimes, so it must exist + // before any of them is created. Emitted first, ahead of both generate loops. + generateInstanceAttach(); + // For each coverpoint, generate sampling code for (AstCoverpoint* cpp : m_coverpoints) generateCoverpointCode(cpp); @@ -537,7 +542,6 @@ class FunctionalCoverageVisitor final : public VNVisitor { const string varName = "__Vprev_" + coverpointp->name(); AstVar* prevVarp = new AstVar{coverpointp->fileline(), VVarType::MEMBER, varName, exprp->dtypep()}; - prevVarp->isStatic(false); m_covergroupp->addMembersp(prevVarp); UINFO(4, " Created previous value variable: " << varName); @@ -562,7 +566,6 @@ class FunctionalCoverageVisitor final : public VNVisitor { // Use 8-bit integer for state position (sequences rarely > 255 items) AstVar* stateVarp = new AstVar{binp->fileline(), VVarType::MEMBER, varName, VFlagLogicPacked{}, 8}; - stateVarp->isStatic(false); m_covergroupp->addMembersp(stateVarp); UINFO(4, " Created sequence state variable: " << varName); @@ -644,17 +647,62 @@ class FunctionalCoverageVisitor final : public VNVisitor { return false; } - // Get (or create) the "VlCoverpointT" member type for hit-list bound K. - AstCDType* vlCoverpointType(FileLine* fl, int hitBound) { - const auto it = m_vlCoverpointTypes.find(hitBound); - if (it != m_vlCoverpointTypes.end()) return it->second; - AstCDType* const typep - = new AstCDType{fl, "VlCoverpointT<" + std::to_string(hitBound) + ">"}; - v3Global.rootp()->typeTablep()->addTypesp(typep); - m_vlCoverpointTypes.emplace(hitBound, typep); + // The interned AstBasicDType for one of the covergroup runtime keywords. + static AstBasicDType* basicDType(FileLine* fl, VBasicDTypeKwd kwd) { + return v3Global.rootp()->typeTablep()->findBasicDType(fl, kwd); + } + + // Get (or create) the coverpoint dtype for a hit-list bound, interned so each distinct bound + // yields one node. + AstCoverpointDType* coverpointDType(FileLine* fl, uint32_t hitBound) { + AstCoverpointDType*& typep = m_cpDTypes[hitBound]; + if (!typep) { + typep = new AstCoverpointDType{fl, hitBound}; + v3Global.rootp()->typeTablep()->addTypesp(typep); + } return typep; } + // Emit the covergroup's instance handle member and the constructor statement that creates + // its node in the per-context coverage registry. Runs before any coverpoint or cross is + // generated, so their runtimes can be added to the node as they are created. + void generateInstanceAttach() { + FileLine* const fl = m_covergroupp->fileline(); + // V3LinkParse synthesizes a 'new' for every covergroup; the item generators below already + // rely on that, and this attach runs even for a covergroup with no coverpoints at all. + UASSERT_OBJ(m_constructorp, m_covergroupp, "Covergroup missing synthesized constructor"); + m_cgInstVarp = new AstVar{fl, VVarType::MEMBER, "__Vcg_inst", + basicDType(fl, VBasicDTypeKwd::COVERGROUP_INSTHANDLE)}; + m_covergroupp->addMembersp(m_cgInstVarp); + + // The type node is keyed by the covergroup type name -- the same string that keys this + // covergroup's coverage-database hierarchy, so it is exactly as unique. Obfuscated the + // same way, so --protect-ids exposes no new identifier. + const std::string typeName + = VIdProtect::protectWordsIf(m_covergroupp->name(), v3Global.opt.protectIds()); + m_constructorp->addStmtsp( + itemCall(fl, m_cgInstVarp, VCMethod::COVERGROUP_ATTACH, + {ctext(fl, "vlSymsp->_vm_contextp__->covergroupRegistryp()" + "->newCovergroupInst(" + + quoted(typeName) + ")")}, + /*usePtr=*/false) + ->makeStmt()); + } + + // Emit 'this->__Vcp_x = this->__Vcg_inst.p()->addCoverpoint();' (or addCross), which + // creates the item runtime in the instance node and borrows a pointer to it. + AstAssign* makeItemCreate(FileLine* fl, AstVar* itemVarp, VCMethod method) { + // '__Vcg_inst.p()' -- a value handle, so '.' not '->' + AstCMethodHard* const instp + = new AstCMethodHard{fl, memberRef(fl, m_cgInstVarp), VCMethod::COVERGROUP_INST_P}; + instp->usePtr(false); + instp->dtypeSetVoid(); // Opaque receiver; only ever the 'fromp' of the call below + AstCMethodHard* const createp = new AstCMethodHard{fl, instp, method}; + createp->usePtr(true); + createp->dtypep(itemVarp->dtypep()); + return new AstAssign{fl, memberRef(fl, itemVarp, VAccess::WRITE), createp}; + } + // Constant bounds of one rangesp() element (an InsideRange or a single Const). Each bound is // the raw AST node -- an AstConst or an AstUnbounded ('$') -- with the const/unbounded view // derived on demand, so there is one source of truth per bound. After a successful @@ -790,12 +838,42 @@ class FunctionalCoverageVisitor final : public VNVisitor { } // A 'this->m_member' reference for embedding in an AstCStmt - AstVarRef* memberRef(FileLine* fl, AstVar* varp) { - AstVarRef* const refp = new AstVarRef{fl, varp, VAccess::READ}; + AstVarRef* memberRef(FileLine* fl, AstVar* varp, VAccess access = VAccess::READ) { + AstVarRef* const refp = new AstVarRef{fl, varp, access}; refp->selfPointer(VSelfPointerText{VSelfPointerText::This{}}); return refp; } + // A 'this->m_member->(args...)' call on one member. usePtr is false only for + // __Vcg_inst, which is a value handle; the item members are borrowed pointers into the + // instance node. Numeric arguments are AstConst; the rest are C++ text that has no AST + // form (see ctext). + AstCMethodHard* itemCall(FileLine* fl, AstVar* varp, VCMethod method, + const std::vector& args = {}, bool usePtr = true) { + AstCMethodHard* const callp = new AstCMethodHard{fl, memberRef(fl, varp), method}; + for (AstNodeExpr* const argp : args) callp->addPinsp(argp); + callp->usePtr(usePtr); + callp->dtypeSetVoid(); + return callp; + } + + // An unsigned integer argument. + static AstConst* cnum(FileLine* fl, uint32_t value) { return new AstConst{fl, value}; } + + // A literal C++ argument with no AST equivalent: a 'const char*' string literal (an SV + // string AstConst emits '"..."s', a std::string temporary the runtime cannot borrow), a + // VlCovBinKind enum token, or a '__V' temporary declared by the enclosing AstCStmt. + static AstCExpr* ctext(FileLine* fl, const std::string& text) { + return new AstCExpr{fl, text}; + } + + // A C++ string literal. Escapes control characters as the emitter does elsewhere -- bin + // names and filenames reach the generated code verbatim when --protect-ids is off, and an + // SV escaped identifier may hold a quote or backslash. + static std::string quoted(const std::string& text) { + return "\"" + V3OutFormatter::quoteNameControls(text) + "\""; + } + // Individual equality targets of an array bin (bins b[] = {values/ranges}), in order. // An open-ended bound ('$', AstUnbounded) resolves to the coverpoint domain: '[lo:$]' // covers [lo:maxVal] and '[$:hi]' covers [0:hi]. One target is produced per value; a @@ -855,32 +933,30 @@ class FunctionalCoverageVisitor final : public VNVisitor { return values; } - // Emit a 'this->m_cp.addSingleNamer/addArrayNamer(...)' statement for one bin - AstCStmt* makeNamer(AstVar* cpVarp, AstCoverBin* binp, int count) { + // Emit a 'this->m_cp->addSingleNamer/addArrayNamer(...)' statement for one bin + AstNodeStmt* makeNamer(AstVar* cpVarp, AstCoverBin* binp, int count) { FileLine* const fl = binp->fileline(); CoverpointBins& bins = m_cpBins.at(cpVarp); const uint32_t normalCount = binp->binsType().binIsNormal() ? static_cast(count < 0 ? 1 : count) : 0; bins.spans.emplace(binp->name(), std::make_pair(bins.total, normalCount)); bins.total += normalCount; - AstCStmt* const cs = new AstCStmt{fl}; - cs->add(memberRef(fl, cpVarp)); // Under --protect-ids the filename and bin name flow into the coverage database // verbatim, so obfuscate them exactly as line/toggle coverage points are (whole- // unit filename, per-word bin name). A no-op when --protect-ids is off. const bool prot = v3Global.opt.protectIds(); - const std::string loc = "\"" + VIdProtect::protectIf(fl->filename(), prot) + "\", " - + std::to_string(fl->lineno()) + ", " - + std::to_string(fl->firstColumn()) + ");"; - const std::string binName = VIdProtect::protectWordsIf(binp->name(), prot); - if (count < 0) { // single bin - cs->add(".addSingleNamer(" + std::string{binp->binsType().binSetEnum()} + ", \"" - + binName + "\", " + loc); - } else { // value array bin - cs->add(".addArrayNamer(" + std::string{binp->binsType().binSetEnum()} + ", " - + std::to_string(count) + ", \"" + binName + "\", " + loc); - } - return cs; + const bool single = count < 0; + std::vector args{ctext(fl, binp->binsType().binSetEnum())}; + if (!single) args.push_back(cnum(fl, static_cast(count))); // value array bin + args.push_back(ctext(fl, quoted(VIdProtect::protectWordsIf(binp->name(), prot)))); + args.push_back(ctext(fl, quoted(VIdProtect::protectIf(fl->filename(), prot)))); + args.push_back(cnum(fl, static_cast(fl->lineno()))); + args.push_back(cnum(fl, static_cast(fl->firstColumn()))); + return itemCall(fl, cpVarp, + single ? VCMethod::COVERGROUP_ADD_SINGLE_NAMER + : VCMethod::COVERGROUP_ADD_ARRAY_NAMER, + args) + ->makeStmt(); } // Emit 'if (iff && cond) m_cp.incrementBin(idx);' (or recordHit, + illegal action) in sample() @@ -894,11 +970,11 @@ class FunctionalCoverageVisitor final : public VNVisitor { // Emit 'this->m_cp.incrementBin(idx);' (Normal) or '.recordHit(idx);' // (ignore/illegal/default). AstNodeStmt* makeRuntimeBinHit(FileLine* fl, const ConvBinTarget& tgt) { - AstCStmt* const cs = new AstCStmt{fl}; - cs->add(memberRef(fl, tgt.cpVarp)); - cs->add((tgt.isNormal ? ".incrementBin(" : ".recordHit(") + std::to_string(tgt.idx) - + ");"); - return cs; + return itemCall(fl, tgt.cpVarp, + tgt.isNormal ? VCMethod::COVERGROUP_INCREMENT_BIN + : VCMethod::COVERGROUP_RECORD_HIT, + {cnum(fl, static_cast(tgt.idx))}) + ->makeStmt(); } void emitConvHitIf(AstCoverpoint* coverpointp, AstCoverBin* binp, AstVar* cpVarp, int idx, @@ -949,26 +1025,25 @@ class FunctionalCoverageVisitor final : public VNVisitor { const int hitBound = computeHitListBound(coverpointp, exprp, crossFed); UINFO(6, " Hit-list bound (max bin overlap) = " << hitBound); AstVar* const cpVarp = new AstVar{fl, VVarType::MEMBER, "__Vcp_" + coverpointp->name(), - vlCoverpointType(fl, hitBound)}; - cpVarp->isStatic(false); + coverpointDType(fl, static_cast(hitBound))}; m_covergroupp->addMembersp(cpVarp); m_cpVars.push_back(cpVarp); m_cpVarMap[coverpointp->name()] = cpVarp; m_cpBins.emplace(cpVarp, CoverpointBins{}); + // Create the runtime in the instance node first; everything below configures it. + m_constructorp->addStmtsp(makeItemCreate(fl, cpVarp, VCMethod::COVERGROUP_ADD_COVERPOINT)); // A cross reads this coverpoint's hit list, so clear it at the start of the // coverpoint's sample() contribution (before any incrementBin appends to it). if (crossFed) { - AstCStmt* const clrp = new AstCStmt{fl}; - clrp->add(memberRef(fl, cpVarp)); - clrp->add(".clearHitList();"); UASSERT_OBJ(m_sampleFuncp, coverpointp, "sample() CFunc not set for clearHitList"); - m_sampleFuncp->addStmtsp(clrp); + m_sampleFuncp->addStmtsp( + itemCall(fl, cpVarp, VCMethod::COVERGROUP_CLEAR_HIT_LIST)->makeStmt()); } // Walk bins (non-default, then default), assigning sequential indices that match the // namer append order; emit sample increments and collect namer statements. - std::vector namerStmts; + std::vector namerStmts; std::vector defaultBins; int idx = 0; for (AstNode* binp = coverpointp->binsp(); binp; binp = binp->nextp()) { @@ -1037,19 +1112,19 @@ class FunctionalCoverageVisitor final : public VNVisitor { const bool prot = v3Global.opt.protectIds(); const std::string hier = VIdProtect::protectWordsIf(m_covergroupp->name() + "." + coverpointp->name(), prot); - AstCStmt* const initp = new AstCStmt{fl}; - initp->add(memberRef(fl, cpVarp)); - initp->add(".init(\"" + hier + "\", " + std::to_string(atLeastValue) + ", " - + std::to_string(idx) + ");"); - m_constructorp->addStmtsp(initp); - for (AstCStmt* const ns : namerStmts) m_constructorp->addStmtsp(ns); + m_constructorp->addStmtsp( + itemCall(fl, cpVarp, VCMethod::COVERGROUP_INIT, + {ctext(fl, quoted(hier)), cnum(fl, static_cast(atLeastValue)), + cnum(fl, static_cast(idx))}) + ->makeStmt()); + for (AstNodeStmt* const ns : namerStmts) m_constructorp->addStmtsp(ns); if (v3Global.opt.coverage()) { const std::string page = VIdProtect::protectIf("v_covergroup/" + m_covergroupp->name(), prot); - AstCStmt* const regp = new AstCStmt{fl}; - regp->add(memberRef(fl, cpVarp)); - regp->add(".registerBins(vlSymsp->_vm_contextp__->coveragep(), \"" + page + "\");"); - m_constructorp->addStmtsp(regp); + m_constructorp->addStmtsp(itemCall(fl, cpVarp, VCMethod::COVERGROUP_REGISTER_BINS, + {ctext(fl, "vlSymsp->_vm_contextp__->coveragep()"), + ctext(fl, quoted(page))}) + ->makeStmt()); } } @@ -1321,34 +1396,53 @@ class FunctionalCoverageVisitor final : public VNVisitor { } } - // Append a "{ VlCoverpoint* __Vcx_cps[] = {&cp0, &cp1, ...}; . }" statement. - AstCStmt* makeCrossCpsCall(FileLine* fl, const std::vector& cpVars, AstVar* cxVarp, - const std::string& callText, - const std::vector& bins = {}) { + // "{ double __Vc = 0.0; double __Vt = 0.0; ->coverageParts(__Vc, __Vt); + // __Vcov += __Vc; __Vtot += __Vt; }" -- one item's contribution to get_coverage(). + // The out-param temporaries make this a block, so only the call itself is a node. + AstCStmt* makeCoveragePartsBlock(FileLine* fl, AstVar* itemVarp) { AstCStmt* const cs = new AstCStmt{fl}; - cs->add("{ "); - if (!bins.empty()) { - cs->add("const bool __Vcx_iffs[] = {"); - bool first = true; - for (const AstCoverCrossBin* const binp : bins) { - if (!first) cs->add(", "); - first = false; - cs->add(binp->iffp() ? binp->iffp()->cloneTree(false) - : new AstConst{fl, AstConst::BitTrue{}}); - } - cs->add("}; "); - } - cs->add("VlCoverpoint* __Vcx_cps[] = {"); - bool first = true; - for (AstVar* const cpVarp : cpVars) { - cs->add(first ? "&" : ", &"); - first = false; - cs->add(memberRef(fl, cpVarp)); + cs->add("{ double __Vc = 0.0; double __Vt = 0.0; "); + cs->add(itemCall(fl, itemVarp, VCMethod::COVERGROUP_COVERAGE_PARTS, + {ctext(fl, "__Vc"), ctext(fl, "__Vt")})); + cs->add("; __Vcov += __Vc; __Vtot += __Vt; }"); + return cs; + } + + // Append a "{ VlCoverpoint* __Vcx_cps[] = {cp0, cp1, ...}; }" statement. The brace + // and the temporary array stay literal text -- a CMethodHard is one call, not a block -- + // but callp itself carries the member, method and '->'. Construction only: init() copies + // the array into the cross, so sample() reads it from there and needs no array at all. + AstCStmt* makeCrossCpsCall(FileLine* fl, const std::vector& cpVars, + AstCMethodHard* callp) { + AstCStmt* const cs = new AstCStmt{fl}; + cs->add("{ VlCoverpoint* __Vcx_cps[] = {"); + for (size_t d = 0; d < cpVars.size(); ++d) { + if (d != 0) cs->add(", "); + cs->add(memberRef(fl, cpVars[d])); } cs->add("}; "); - cs->add(memberRef(fl, cxVarp)); - cs->add(callText); - cs->add(" }"); + cs->add(callp); + cs->add("; }"); + return cs; + } + + // Append a "{ const bool __Vcx_iffs[] = {, ...}; }" statement, one entry per + // explicit cross bin in declaration order (true where the bin has no iff). As above, the + // temporary array is literal text because a CMethodHard is one call, not a block. + AstCStmt* makeCrossIffsCall(FileLine* fl, const std::vector& bins, + AstCMethodHard* callp) { + AstCStmt* const cs = new AstCStmt{fl}; + cs->add("{ const bool __Vcx_iffs[] = {"); + bool first = true; + for (const AstCoverCrossBin* const binp : bins) { + if (!first) cs->add(", "); + first = false; + cs->add(binp->iffp() ? binp->iffp()->cloneTree(false) + : new AstConst{fl, AstConst::BitTrue{}}); + } + cs->add("}; "); + cs->add(callp); + cs->add("; }"); return cs; } @@ -1393,24 +1487,20 @@ class FunctionalCoverageVisitor final : public VNVisitor { if (!count) continue; FileLine* const fl = binp->fileline(); const bool prot = v3Global.opt.protectIds(); - const std::string name = V3OutFormatter::quoteNameControls( - VIdProtect::protectWordsIf(binp->name(), prot)); - const std::string file - = V3OutFormatter::quoteNameControls(VIdProtect::protectIf(fl->filename(), prot)); - AstCStmt* const addp = new AstCStmt{fl}; - addp->add(memberRef(fl, cxVarp)); - addp->add(".addBin(" + std::to_string(dim) + ", " + std::to_string(first) + ", " - + std::to_string(count) + ", \"" + name + "\", \"" + file + "\", " - + std::to_string(fl->lineno()) + ", " + std::to_string(fl->firstColumn()) - + ");"); - m_constructorp->addStmtsp(addp); + m_constructorp->addStmtsp( + itemCall(fl, cxVarp, VCMethod::COVERGROUP_ADD_BIN, + {cnum(fl, dim), cnum(fl, first), cnum(fl, count), + ctext(fl, quoted(VIdProtect::protectWordsIf(binp->name(), prot))), + ctext(fl, quoted(VIdProtect::protectIf(fl->filename(), prot))), + cnum(fl, static_cast(fl->lineno())), + cnum(fl, static_cast(fl->firstColumn()))}) + ->makeStmt()); bins.push_back(binp); } if (!bins.empty()) { - AstCStmt* const finishp = new AstCStmt{crossp->fileline()}; - finishp->add(memberRef(crossp->fileline(), cxVarp)); - finishp->add(".finalizeBins();"); - m_constructorp->addStmtsp(finishp); + m_constructorp->addStmtsp( + itemCall(crossp->fileline(), cxVarp, VCMethod::COVERGROUP_FINALIZE_BINS) + ->makeStmt()); } return bins; } @@ -1443,42 +1533,47 @@ class FunctionalCoverageVisitor final : public VNVisitor { } const int dims = static_cast(cpVars.size()); - if (!m_vlCoverCrossDTypep) { - m_vlCoverCrossDTypep = new AstCDType{fl, "VlCoverCross"}; - v3Global.rootp()->typeTablep()->addTypesp(m_vlCoverCrossDTypep); - } - AstVar* const cxVarp - = new AstVar{fl, VVarType::MEMBER, "__Vcx_" + crossp->name(), m_vlCoverCrossDTypep}; - cxVarp->isStatic(false); + AstVar* const cxVarp = new AstVar{fl, VVarType::MEMBER, "__Vcx_" + crossp->name(), + basicDType(fl, VBasicDTypeKwd::COVERGROUP_CROSS)}; m_covergroupp->addMembersp(cxVarp); m_crossVars.push_back(cxVarp); + m_constructorp->addStmtsp(makeItemCreate(fl, cxVarp, VCMethod::COVERGROUP_ADD_CROSS)); // Constructor: init (after the coverpoints, which generate earlier) then registration. // Obfuscate the hierarchy/filename/page under --protect-ids as for coverpoints above. const bool prot = v3Global.opt.protectIds(); const std::string hier = VIdProtect::protectWordsIf(m_covergroupp->name() + "." + crossp->name(), prot); - const std::string initCall - = ".init(\"" + hier + "\", " + std::to_string(dims) + ", __Vcx_cps, \"" - + VIdProtect::protectIf(fl->filename(), prot) + "\", " + std::to_string(fl->lineno()) - + ", " + std::to_string(fl->firstColumn()) + ");"; - m_constructorp->addStmtsp(makeCrossCpsCall(fl, cpVars, cxVarp, initCall)); + m_constructorp->addStmtsp(makeCrossCpsCall( + fl, cpVars, + itemCall(fl, cxVarp, VCMethod::COVERGROUP_INIT, + {ctext(fl, quoted(hier)), cnum(fl, static_cast(dims)), + ctext(fl, "__Vcx_cps"), + ctext(fl, quoted(VIdProtect::protectIf(fl->filename(), prot))), + cnum(fl, static_cast(fl->lineno())), + cnum(fl, static_cast(fl->firstColumn()))}))); const std::vector bins = generateCrossBins(crossp, cxVarp, cpVars, dimensions); if (v3Global.opt.coverage()) { const std::string page = VIdProtect::protectIf("v_covergroup/" + m_covergroupp->name(), prot); - AstCStmt* const regp = new AstCStmt{fl}; - regp->add(memberRef(fl, cxVarp)); - regp->add(".registerBins(vlSymsp->_vm_contextp__->coveragep(), \"" + page + "\");"); - m_constructorp->addStmtsp(regp); + m_constructorp->addStmtsp(itemCall(fl, cxVarp, VCMethod::COVERGROUP_REGISTER_BINS, + {ctext(fl, "vlSymsp->_vm_contextp__->coveragep()"), + ctext(fl, quoted(page))}) + ->makeStmt()); } // sample(): after all coverpoints have sampled (cross loop runs after coverpoint loop). UASSERT_OBJ(m_sampleFuncp, crossp, "sample() CFunc not set for cross"); - AstNodeStmt* const samplep = makeCrossCpsCall( - fl, cpVars, cxVarp, - bins.empty() ? ".sample(__Vcx_cps);" : ".sample(__Vcx_cps, __Vcx_iffs);", bins); + // The cross reads its coverpoints from its own m_cps, so sample() needs no cps array; + // per-bin iff guards still need a temporary array, hence the block form. + AstNodeStmt* const samplep + = bins.empty() ? static_cast( + itemCall(fl, cxVarp, VCMethod::COVERGROUP_SAMPLE)->makeStmt()) + : static_cast(makeCrossIffsCall( + fl, bins, + itemCall(fl, cxVarp, VCMethod::COVERGROUP_SAMPLE_IFFS, + {ctext(fl, "__Vcx_iffs")}))); if (AstNodeExpr* const iffp = crossp->iffp()) { m_sampleFuncp->addStmtsp(new AstIf{fl, iffp->cloneTree(false), samplep}); } else { @@ -1693,19 +1788,11 @@ class FunctionalCoverageVisitor final : public VNVisitor { headp->add("double __Vcov = 0.0; double __Vtot = 0.0;"); funcp->addStmtsp(headp); for (AstVar* const cpVarp : m_cpVars) { - AstCStmt* const cs = new AstCStmt{fl}; - cs->add("{ double __Vc = 0.0; double __Vt = 0.0; "); - cs->add(memberRef(fl, cpVarp)); - cs->add(".coverageParts(__Vc, __Vt); __Vcov += __Vc; __Vtot += __Vt; }"); - funcp->addStmtsp(cs); + funcp->addStmtsp(makeCoveragePartsBlock(fl, cpVarp)); } // Crosses contribute the same covered/total ratio as their per-tuple bins. for (AstVar* const cxVarp : m_crossVars) { - AstCStmt* const cs = new AstCStmt{fl}; - cs->add("{ double __Vc = 0.0; double __Vt = 0.0; "); - cs->add(memberRef(fl, cxVarp)); - cs->add(".coverageParts(__Vc, __Vt); __Vcov += __Vc; __Vtot += __Vt; }"); - funcp->addStmtsp(cs); + funcp->addStmtsp(makeCoveragePartsBlock(fl, cxVarp)); } AstCStmt* const retp = new AstCStmt{fl}; retp->add(new AstVarRef{fl, returnVarp, VAccess::WRITE}); @@ -1936,7 +2023,6 @@ class FunctionalCoverageVisitor final : public VNVisitor { = trigger.memberVarp ? trigger.memberVarp->dtypep() : trigger.baseVarp->dtypep(); AstVar* const prevVarp = new AstVar{trigger.eventFl, VVarType::MEMBER, eventPrevName(trigger, triggerIndex), dtypep}; - prevVarp->isStatic(false); m_enclosingClassp->addMembersp(prevVarp); trigger.prevVarp = prevVarp; for (AstNodeAssign* const asgnp : assignps) { @@ -2102,7 +2188,6 @@ class FunctionalCoverageVisitor final : public VNVisitor { v3Global.rootp()->typeTablep()->addTypesp(enclDTypep); AstVar* const handleVarp = new AstVar{fl, VVarType::MEMBER, "__Vcg_enclosingp", enclDTypep}; - handleVarp->isStatic(false); m_covergroupp->addMembersp(handleVarp); // Route each enclosing-member reference through the back-pointer: 'm' -> 'h.m'. diff --git a/src/V3EmitCFunc.cpp b/src/V3EmitCFunc.cpp index 5aa4dca5e..e285a3c4a 100644 --- a/src/V3EmitCFunc.cpp +++ b/src/V3EmitCFunc.cpp @@ -541,6 +541,8 @@ string EmitCFunc::emitVarResetRecurse(const AstVar* varp, bool constructing, depth + 1, suffix + ".atDefault()", nullptr); } else if (VN_IS(dtypep, CDType)) { return ""; // Constructor does it + } else if (VN_IS(dtypep, CoverpointDType)) { + return ""; // Covergroup constructor creates the runtime and assigns the pointer } else if (const AstClassRefDType* const adtypep = VN_CAST(dtypep, ClassRefDType)) { return adtypep->rawPointer() ? varNameProtected + suffix + " = nullptr;\n" : ""; } else if (VN_IS(dtypep, IfaceRefDType)) { @@ -594,6 +596,10 @@ string EmitCFunc::emitVarResetRecurse(const AstVar* varp, bool constructing, return ""; } else if (basicp && (basicp->isRandomGenerator() || basicp->isStdRandomGenerator())) { return ""; + } else if (basicp && (basicp->isCovergroupInstHandle() || basicp->isCovergroupCross())) { + // The handle's own constructor deals with it; the cross is a borrowed pointer that + // the covergroup constructor assigns once it creates the runtime + return ""; } else if (basicp && (basicp->isEvent())) { return "VlAssignableEvent{};\n"; } else if (basicp) { diff --git a/src/V3EmitCFunc.h b/src/V3EmitCFunc.h index bea412aa9..f1e8eb92c 100644 --- a/src/V3EmitCFunc.h +++ b/src/V3EmitCFunc.h @@ -813,6 +813,12 @@ public: const AstUnpackArrayDType* const adtypep = VN_AS(nodep->dtypep()->skipRefp(), UnpackArrayDType); puts("<" + cvtToStr(adtypep->elementsConst()) + ">"); + } else if (nodep->method() == VCMethod::COVERGROUP_ADD_COVERPOINT) { + // The hit-list bound is a template argument of the returned VlCoverpointT<>, and the + // node's own dtype is that type, so it is the one source of truth for both. + const AstCoverpointDType* const cpdtypep + = VN_AS(nodep->dtypep()->skipRefp(), CoverpointDType); + puts("<" + cvtToStr(cpdtypep->hitBound()) + ">"); } puts("("); bool comma = false; diff --git a/test_regress/t/t_covergroup_inst_churn.py b/test_regress/t/t_covergroup_inst_churn.py new file mode 100755 index 000000000..8d482549e --- /dev/null +++ b/test_regress/t/t_covergroup_inst_churn.py @@ -0,0 +1,25 @@ +#!/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_all') + +# CHURN + 3 clock edges at 10 time units each. The default 1100 gives ~110 edges, +# far short of the churn the sentinel needs to be convincing. +test.sim_time = 25000 + +# Deliberately WITHOUT --coverage: under --coverage the coverage database holds +# raw pointers into each instance's counts and nodes must be retained instead +# (see t_covergroup_inst_lifetime). Freeing is unlocked only without coverage. +test.compile() + +test.execute() + +test.passes() diff --git a/test_regress/t/t_covergroup_inst_churn.v b/test_regress/t/t_covergroup_inst_churn.v new file mode 100644 index 000000000..2a58263dc --- /dev/null +++ b/test_regress/t/t_covergroup_inst_churn.v @@ -0,0 +1,102 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Matthew Ballance +// SPDX-License-Identifier: CC0-1.0 + +// SENTINEL: covergroup instance nodes must not accumulate. +// +// A covergroup instance's bins live in the per-context registry, not the SV +// object, so dropping the last SV handle releases nothing by itself. This +// creates and drops CHURN instances while holding at most one handle, and fails +// if the live-node count tracks instances ever created rather than reachable. +// A memory test without measuring memory: the live count is exact and +// deterministic where an RSS threshold is neither. +// +// Catches only accumulation-without-leaking, which LeakSanitizer cannot see. +// Unlink-without-free is t_covergroup_inst_retire_asan's job, so this file has +// no ASAN variant. What it uniquely adds is scale: enough instances that O(N) +// versus O(1) is unmistakable. +// +// Instances are dropped across clock edges: garbage is deleted at the start of +// the next eval_step, so a single-eval test would free nothing. + +module t ( + input clk +); + + // Enough that accumulation is unmistakable, while staying under a second. + localparam int CHURN = 2000; + + // Nodes alive at once: one being sampled, one awaiting collection. Loose on + // purpose -- the assertion is O(1) versus O(CHURN), not an exact constant. + localparam int LIVE_MAX = 4; + + int cyc = 0; + logic [1:0] v; + + int live = 0; + int peak_live = 0; + int created = 0; + + covergroup cg_churn; + cp: coverpoint v { + bins b0 = {0}; + bins b1 = {1}; + bins b2 = {2}; + bins b3 = {3}; + } + endgroup + + cg_churn cg; + + always @(posedge clk) begin + cyc <= cyc + 1; + + if (cyc < CHURN) begin + cg = new; + v = cyc[1:0]; + cg.sample(); + + // Live when sampled, so a working registry reports >= 1. Zero would mean + // the node was never registered and the rest of this test proves nothing. + live = $c32("Verilated::threadContextp()->covergroupRegistryp()->liveInstanceCount()"); + if (live > peak_live) peak_live = live; + + cg = null; // Last handle dropped; collected at the next eval_step + end else if (cyc == CHURN + 2) begin + // Two edges after the final drop, so the last instance has been collected. + live = $c32("Verilated::threadContextp()->covergroupRegistryp()->liveInstanceCount()"); + created = $c32("Verilated::threadContextp()->covergroupRegistryp()->createdInstanceCount()"); + + // Guard: a build that folded the covergroup away would report live == 0 + // and "pass". + if (created != CHURN) begin + $display("%%Error: created %0d covergroup instances, expected %0d", created, CHURN); + $stop; + end + + if (peak_live < 1) begin + $display("%%Error: never observed a live instance; the probe is not measuring anything"); + $stop; + end + + if (peak_live > LIVE_MAX) begin + $display("%%Error: covergroup instances accumulate: peak %0d live nodes over %0d", + peak_live, CHURN); + $display(" Expected no more than %0d live at once. Instance nodes are not", + LIVE_MAX); + $display(" released when the last SV handle drops."); + $stop; + end + + if (live != 0) begin + $display("%%Error: %0d covergroup instances still live after all handles dropped", live); + $stop; + end + + $write("*-* All Finished *-*\n"); + $finish; + end + end +endmodule diff --git a/test_regress/t/t_covergroup_inst_handle.cpp b/test_regress/t/t_covergroup_inst_handle.cpp new file mode 100644 index 000000000..bde213526 --- /dev/null +++ b/test_regress/t/t_covergroup_inst_handle.cpp @@ -0,0 +1,101 @@ +// -*- mode: C++; c-file-style: "cc-mode" -*- +// +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Matthew Ballance +// SPDX-License-Identifier: CC0-1.0 + +// VlCovInstHandle properties no SystemVerilog covergroup can produce. +// +// 1. An attach count above 1. The handle's copy constructor is reached only +// through a generated clone(), i.e. 'cg c2 = new c1;' -- a construct +// Verilator is slated to reject. Testing attach counting through it would +// mean a test whose reason to exist is scheduled for removal, so the count +// is exercised directly here. The copy constructor must still compile: +// every generated class clone()s its members. +// +// 2. A handle outliving the registry. Examples declare the context before the +// model, so the registry is alive when the last handle drops; this is the +// other order. Handle destructors reach into the registry, so +// ~VlCovRegistry cannot free the nodes -- that would turn a harmless usage +// error into a use-after-free on the node. It leaks attached types instead. +// +// The model makes case 2 realistic and is deliberately never deleted: deleting +// it after the context runs ~VerilatedSyms, whose checkMagic() on the freed +// context is a pre-existing runtime use-after-free unrelated to covergroups. +// +// Run under -fsanitize=address; both cases fail as use-after-free. + +#include +#include + +#include VM_PREFIX_INCLUDE + +#include + +double sc_time_stamp() { return 0; } + +int errors = 0; + +void checkEq(const char* what, uint32_t got, uint32_t exp) { + if (got != exp) { + printf("%%Error: %s: got=%u exp=%u\n", what, got, exp); + ++errors; + } +} + +int main(int argc, char* argv[]) { + VerilatedContext* const contextp = new VerilatedContext; + contextp->commandArgs(argc, argv); + + VM_PREFIX* const topp = new VM_PREFIX{contextp, "top"}; + + VlCovRegistry* const registryp = contextp->covergroupRegistryp(); + + { + // ---- Case 1: attach count above 1 ---- + VlCovInstHandle first; + first.attach(registryp->newCovergroupInst("cg_handle")); + checkEq("one handle, one node", registryp->liveInstanceCount("cg_handle"), 1); + checkEq("nothing retired yet", registryp->retiredInstanceCount("cg_handle"), 0); + + { + // Copy-construction shares the node; it does not make a second one. + const VlCovInstHandle second{first}; // NOLINT: exercising the copy ctor + checkEq("copy shares the node", registryp->liveInstanceCount("cg_handle"), 1); + checkEq("copy created nothing", registryp->createdInstanceCount("cg_handle"), 1); + } + // 'second' is gone, 'first' is not. The node must still be here: an + // attach count that never rose would have retired it on that drop, and + // the queries below would then be reading freed storage. + checkEq("node outlives the copy", registryp->liveInstanceCount("cg_handle"), 1); + checkEq("nothing retired on the copy", registryp->retiredInstanceCount("cg_handle"), 0); + + // ---- Case 2: a handle outliving the registry ---- + // Give the design its covergroup instance first, so the registry has a + // second attached type when it is destroyed. + for (int i = 0; i < 8; ++i) { + topp->clk = i & 1; + topp->eval(); + contextp->timeInc(1); + } + + delete contextp; // NOLINT: the wrong destruction order is the point + + // ~first runs at the end of this block: attachDec() on a node whose + // registry is already gone. Nothing may query the registry from here on + // -- it no longer exists -- so the check is simply that this is not a + // use-after-free. + } + + // topp is deliberately leaked; see the header comment. + (void)topp; + + if (errors) { + printf("%%Error: %d failure(s)\n", errors); + return 10; + } + printf("*-* All Finished *-*\n"); + return 0; +} diff --git a/test_regress/t/t_covergroup_inst_handle.py b/test_regress/t/t_covergroup_inst_handle.py new file mode 100755 index 000000000..269ced94a --- /dev/null +++ b/test_regress/t/t_covergroup_inst_handle.py @@ -0,0 +1,44 @@ +#!/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') + +# Hand-written harness: neither property is reachable from SystemVerilog -- an +# attach count above 1 needs the handle's copy constructor, and a handle +# outliving the registry needs the context destroyed before the model. See the +# .cpp. +# +# Built WITHOUT --coverage, so retirement is on the free path, and with +# AddressSanitizer: freeing a node under a live handle is a use-after-free that +# without a sanitizer is a segfault or a silent pass depending on the allocator. +# -fsanitize=address is needed on both CFLAGS (to instrument) and LDFLAGS (to +# link the runtime). +# Not --runtime-debug: its extra checks add nothing here and triple the runtime +# recompile. ASAN is incompatible with TSAN, which --runtime-debug would have +# made driver.py notice for us. +if test.tsan: + test.skip("ThreadSanitizer not compatible with AddressSanitizer\n") + +test.compile(make_top_shell=False, + make_main=False, + verilator_flags2=[ + "--exe", "-CFLAGS -fsanitize=address -LDFLAGS -fsanitize=address", + test.pli_filename + ]) + +# LeakSanitizer off: this test leaks on purpose -- the registry leaks the type it +# may not free, the harness leaks the model it must not delete. Both are the +# subject of the test. Every other check, use-after-free included, stays on. +test.leak_check_disable() + +test.execute() + +test.passes() diff --git a/test_regress/t/t_covergroup_inst_handle.v b/test_regress/t/t_covergroup_inst_handle.v new file mode 100644 index 000000000..2c80a8ac4 --- /dev/null +++ b/test_regress/t/t_covergroup_inst_handle.v @@ -0,0 +1,43 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Matthew Ballance +// SPDX-License-Identifier: CC0-1.0 + +// Design half of the VlCovInstHandle test. All this has to do is create a +// covergroup instance and never let go of it, so that the registry still has an +// attached node when the C++ harness destroys the context. +// +// See t_covergroup_inst_handle.cpp for what is actually being tested. + +module t ( + input clk +); + + int cyc = 0; + logic [1:0] v; + + covergroup cg_ctx; + cp: coverpoint v { + bins b0 = {0}; + bins b1 = {1}; + } + endgroup + + cg_ctx g; + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc == 0) begin + g = new; + v = 0; + g.sample(); + end else if (g == null) begin + // Never taken. It exists to *read* g: a covergroup handle that is written + // and never read is localized into this block, and the instance would then + // be dropped at the end of the edge that created it -- leaving nothing + // attached, and this test measuring nothing. + $stop; + end + end +endmodule diff --git a/test_regress/t/t_covergroup_inst_lifetime.out b/test_regress/t/t_covergroup_inst_lifetime.out new file mode 100644 index 000000000..0c49ade11 --- /dev/null +++ b/test_regress/t/t_covergroup_inst_lifetime.out @@ -0,0 +1,4 @@ +cg_life.cp.b0: 4 +cg_life.cp.b1: 3 +cg_life.cp.b2: 2 +cg_life.cp.b3: 17 diff --git a/test_regress/t/t_covergroup_inst_lifetime.py b/test_regress/t/t_covergroup_inst_lifetime.py new file mode 100755 index 000000000..6b0f1a9e4 --- /dev/null +++ b/test_regress/t/t_covergroup_inst_lifetime.py @@ -0,0 +1,15 @@ +#!/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 +import coverage_covergroup_common + +test.scenarios('vlt') + +coverage_covergroup_common.run(test) diff --git a/test_regress/t/t_covergroup_inst_lifetime.v b/test_regress/t/t_covergroup_inst_lifetime.v new file mode 100644 index 000000000..205309529 --- /dev/null +++ b/test_regress/t/t_covergroup_inst_lifetime.v @@ -0,0 +1,56 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Matthew Ballance +// SPDX-License-Identifier: CC0-1.0 + +// Covergroup instances that are dropped before the run ends. Their bins have +// already been registered with the coverage database, so their counts must +// still appear -- and be correct -- in the final report. +// +// Instances are dropped across clock edges on purpose: garbage objects are +// deleted at the start of the *next* eval_step, so a single-eval test would +// never actually free them and would not exercise this at all. + +module t ( + input clk +); + + int cyc = 0; + logic [1:0] v; + + covergroup cg_life; + cp: coverpoint v { + bins b0 = {0}; + bins b1 = {1}; + bins b2 = {2}; + bins b3 = {3}; + } + endgroup + + cg_life cg; + + always @(posedge clk) begin + cyc <= cyc + 1; + if (cyc < 4) begin + // Instance 'cyc' samples values 0..cyc, so the per-bin totals across all + // four instances are b0=4, b1=3, b2=2, b3=1. + cg = new; + for (int j = 0; j <= cyc; ++j) begin + v = j[1:0]; + cg.sample(); + end + cg = null; // last handle dropped; freed at the next eval_step + end else if (cyc < 20) begin + // Churn: each of these reuses the freed storage of an earlier instance, + // so a stale count pointer reads a live instance's counter instead. + cg = new; + v = 2'b11; + cg.sample(); + cg = null; + end else if (cyc == 20) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end +endmodule diff --git a/test_regress/t/t_covergroup_inst_retain.out b/test_regress/t/t_covergroup_inst_retain.out new file mode 100644 index 000000000..1bd17eeba --- /dev/null +++ b/test_regress/t/t_covergroup_inst_retain.out @@ -0,0 +1,4 @@ +cg_retain.cp.b0: 1 +cg_retain.cp.b1: 1 +cg_retain.cp.b2: 1 +cg_retain.cp.b3: 0 diff --git a/test_regress/t/t_covergroup_inst_retain.py b/test_regress/t/t_covergroup_inst_retain.py new file mode 100755 index 000000000..01e5d172b --- /dev/null +++ b/test_regress/t/t_covergroup_inst_retain.py @@ -0,0 +1,29 @@ +#!/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 +import coverage_covergroup_common + +test.scenarios('vlt') + +# WITH --coverage, which is the point: it turns off the free path in +# VlCovergroupType::retire(). The golden report is the other half -- both +# instances' bins must survive the design dropping both handles. +# +# Also AddressSanitizer, rather than a separate _asan variant: this is the only +# retain-path test, and a wrong free there is a use-after-free the golden +# report would catch only if freed storage read back as a wrong number. Not +# --runtime-debug: its extra checks add nothing here and triple the runtime +# recompile. ASAN is incompatible with TSAN, which --runtime-debug would have +# made driver.py notice for us. +if test.tsan: + test.skip("ThreadSanitizer not compatible with AddressSanitizer\n") + +coverage_covergroup_common.run( + test, verilator_flags2=["-CFLAGS -fsanitize=address -LDFLAGS -fsanitize=address"]) diff --git a/test_regress/t/t_covergroup_inst_retain.v b/test_regress/t/t_covergroup_inst_retain.v new file mode 100644 index 000000000..fb7c63c14 --- /dev/null +++ b/test_regress/t/t_covergroup_inst_retain.v @@ -0,0 +1,124 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Matthew Ballance +// SPDX-License-Identifier: CC0-1.0 + +// Retirement under --coverage: the node is kept, but stops counting as live. +// +// Under --coverage, registerBins() has given the coverage database raw pointers +// into each instance's bin counts, read at write() time, so the free path is off +// and a retired instance is only marked dead. +// +// t_covergroup_inst_lifetime pins the reported half of that contract (a dead +// instance's counts still appear). This pins the other half: a retained node +// must not count as live. Every other retirement test builds without +// --coverage, so an implementation that returned m_insts.size() would pass the +// rest of the suite. +// +// Handles are dropped and checked two clock edges later; garbage is deleted at +// the start of a later eval_step. + +// 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 checkalive(hv) do if ((hv) == null) begin $write("%%Error: %s:%0d: handle is null\n", `__FILE__,`__LINE__); `stop; end while(0); +// verilog_format: on + +module t ( + input clk +); + + int cyc = 0; + logic [1:0] v; + + covergroup cg_retain; + cp: coverpoint v { + bins b0 = {0}; + bins b1 = {1}; + bins b2 = {2}; + bins b3 = {3}; + } + endgroup + + // Every handle below is read through `checkalive before being dropped: a + // handle written and never read is localized and dies in its creating edge. + cg_retain r1, r2; + + function int live(); + live = $c32( + "Verilated::threadContextp()->covergroupRegistryp()->liveInstanceCount(\"cg_retain\")"); + endfunction + function int created(); + created = $c32( + "Verilated::threadContextp()->covergroupRegistryp()->createdInstanceCount(\"cg_retain\")"); + endfunction + function int retired(); + retired = $c32( + "Verilated::threadContextp()->covergroupRegistryp()->retiredInstanceCount(\"cg_retain\")"); + endfunction + function int retired_cov_x100(); + retired_cov_x100 = $c32( + "(int)(Verilated::threadContextp()->covergroupRegistryp()", + "->retiredCoverage(\"cg_retain\") * 100.0 + 0.5)"); + endfunction + + // Hit bins lo .. hi. Sampling from a loop, not straight-line assignments. + task automatic sample_range(cg_retain cg, int lo, int hi); + for (int i = lo; i <= hi; ++i) begin + v = i[1:0]; + cg.sample(); + end + endtask + + always @(posedge clk) begin + cyc <= cyc + 1; + case (cyc) + 0: begin + r1 = new; + sample_range(r1, 0, 1); // 2 of 4 bins -> 50% + end + + 2: begin + `checkd(live(), 1); + `checkd(created(), 1); + `checkd(retired(), 0); + `checkalive(r1); + r1 = null; + end + + 4: begin + // The node is still there, but dead. A liveInstanceCount() returning + // m_insts.size() would read 1 here. + `checkd(live(), 0); + `checkd(created(), 1); + `checkd(retired(), 1); + `checkd(retired_cov_x100(), 5000); + + r2 = new; + sample_range(r2, 2, 2); // 1 of 4 bins -> 25% + end + + 6: begin + // A live instance alongside a retained dead one: exactly one is live. + `checkd(live(), 1); + `checkd(created(), 2); + `checkd(retired(), 1); + `checkalive(r2); + r2 = null; + end + + 8: begin + `checkd(live(), 0); + `checkd(created(), 2); + `checkd(retired(), 2); + `checkd(retired_cov_x100(), 3750); // mean of 50% and 25% + + $write("*-* All Finished *-*\n"); + $finish; + end + + default: ; + endcase + end +endmodule diff --git a/test_regress/t/t_covergroup_inst_retire.py b/test_regress/t/t_covergroup_inst_retire.py new file mode 100755 index 000000000..4a59c9219 --- /dev/null +++ b/test_regress/t/t_covergroup_inst_retire.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('vlt_all') + +# Deliberately compiled WITHOUT --coverage, which is what unlocks the free path +# in VlCovergroupType::retire(). Under --coverage the node is retained instead; +# t_covergroup_inst_lifetime pins that side. +test.compile() + +test.execute() + +test.passes() diff --git a/test_regress/t/t_covergroup_inst_retire.v b/test_regress/t/t_covergroup_inst_retire.v new file mode 100644 index 000000000..bc8859655 --- /dev/null +++ b/test_regress/t/t_covergroup_inst_retire.v @@ -0,0 +1,275 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Matthew Ballance +// SPDX-License-Identifier: CC0-1.0 + +// Covergroup instance retirement: registry bookkeeping and the dead instance's +// residue when the last SV handle drops. +// +// t_covergroup_inst_churn proves nodes do not accumulate, but never holds more +// than one instance, so the retired slot is always both first and last and the +// erase-by-swap fixup is never exercised. This test exercises it, and pins the +// residue values. +// +// Built WITHOUT --coverage, so retirement takes the free path; +// t_covergroup_inst_lifetime pins the retained side. Handles are dropped and +// checked two clock edges later, since garbage is deleted in a later eval_step. + +// 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 checkr(gotv,expv) do if ((((gotv) - (expv)) > 0.001) || (((expv) - (gotv)) > 0.001)) begin $write("%%Error: %s:%0d: got=%f exp=%f\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +`define checkalive(hv) do if ((hv) == null) begin $write("%%Error: %s:%0d: handle is null\n", `__FILE__,`__LINE__); `stop; end while(0); +// verilog_format: on + +module t ( + input clk +); + + int cyc = 0; + logic [1:0] v; + + // Three covergroup types, so each scenario's residue is read in isolation: + // the accessors are per-type, and one shared type would average the three + // scenarios together and pin nothing. + + // Slot bookkeeping under erase-by-swap. Several instances live at once. + covergroup cg_slot; + cp: coverpoint v { + bins b0 = {0}; + bins b1 = {1}; + bins b2 = {2}; + bins b3 = {3}; + } + endgroup + + // Residue value: one instance, partially covered, harvested on death. + covergroup cg_fold; + cp: coverpoint v { + bins b0 = {0}; + bins b1 = {1}; + bins b2 = {2}; + bins b3 = {3}; + } + endgroup + + // Residue of an instance that was never sampled. + covergroup cg_none; + cp: coverpoint v { + bins b0 = {0}; + bins b1 = {1}; + bins b2 = {2}; + bins b3 = {3}; + } + endgroup + + cg_slot a, b, c, d, e, f; + cg_fold g; + cg_none h; + + // Every handle below is read through `checkalive before being dropped: a + // handle written and never read is localized and dies in its creating edge. + + // The registry accessors are C++-only on purpose: they are test and debug + // observability, not an SV-visible surface, so they are reached through $c. + // The type name is the covergroup's name as the code generator emits it. + function int slot_live(); + slot_live = $c32( + "Verilated::threadContextp()->covergroupRegistryp()->liveInstanceCount(\"cg_slot\")"); + endfunction + function int slot_retired(); + slot_retired = $c32( + "Verilated::threadContextp()->covergroupRegistryp()->retiredInstanceCount(\"cg_slot\")"); + endfunction + // Coverage in hundredths of a percent, so the checks are integer-exact rather + // than float comparisons smuggled through $c. + function int slot_retired_cov_x100(); + slot_retired_cov_x100 = $c32( + "(int)(Verilated::threadContextp()->covergroupRegistryp()", + "->retiredCoverage(\"cg_slot\") * 100.0 + 0.5)"); + endfunction + function int fold_retired(); + fold_retired = $c32( + "Verilated::threadContextp()->covergroupRegistryp()->retiredInstanceCount(\"cg_fold\")"); + endfunction + function int fold_retired_cov_x100(); + fold_retired_cov_x100 = $c32( + "(int)(Verilated::threadContextp()->covergroupRegistryp()", + "->retiredCoverage(\"cg_fold\") * 100.0 + 0.5)"); + endfunction + function int none_retired(); + none_retired = $c32( + "Verilated::threadContextp()->covergroupRegistryp()->retiredInstanceCount(\"cg_none\")"); + endfunction + function int none_retired_cov_x100(); + none_retired_cov_x100 = $c32( + "(int)(Verilated::threadContextp()->covergroupRegistryp()", + "->retiredCoverage(\"cg_none\") * 100.0 + 0.5)"); + endfunction + // retiredCoverage() reports "no data" as a negative, distinct from 0.0; the + // x100 helpers cannot tell the two apart. + function int slot_retired_cov_is_none(); + slot_retired_cov_is_none = $c32( + "(Verilated::threadContextp()->covergroupRegistryp()", + "->retiredCoverage(\"cg_slot\") < 0.0 ? 1 : 0)"); + endfunction + + // Queries against a type the registry has never heard of: 0 / 0 / 0 / negative. + function int unknown_live(); + unknown_live = $c32( + "Verilated::threadContextp()->covergroupRegistryp()", + "->liveInstanceCount(\"cg_no_such_type\")"); + endfunction + function int unknown_created(); + unknown_created = $c32( + "Verilated::threadContextp()->covergroupRegistryp()", + "->createdInstanceCount(\"cg_no_such_type\")"); + endfunction + function int unknown_retired(); + unknown_retired = $c32( + "Verilated::threadContextp()->covergroupRegistryp()", + "->retiredInstanceCount(\"cg_no_such_type\")"); + endfunction + function int unknown_cov_is_none(); + unknown_cov_is_none = $c32( + "(Verilated::threadContextp()->covergroupRegistryp()", + "->retiredCoverage(\"cg_no_such_type\") < 0.0 ? 1 : 0)"); + endfunction + + // Hit every bin of one cg_slot instance, so get_inst_coverage() reads 100%. + task automatic sample_all(cg_slot cg); + for (int i = 0; i < 4; ++i) begin + v = i[1:0]; + cg.sample(); + end + endtask + + // Hit 'nbins' of cg_fold's four bins. + task automatic sample_fold(cg_fold cg, int nbins); + for (int i = 0; i < nbins; ++i) begin + v = i[1:0]; + cg.sample(); + end + endtask + + always @(posedge clk) begin + cyc <= cyc + 1; + case (cyc) + 0: begin + // Slots 0,1,2,3. + a = new; + b = new; + c = new; + d = new; + + // cg_fold: 2 of 4 bins -> 50%. Sampled while live; the fold must read + // these counts before the node is unlinked, not after. + g = new; + sample_fold(g, 2); + + // cg_none: created and never sampled. + h = new; + end + + 2: begin + `checkd(slot_live(), 4); + `checkd(slot_retired(), 0); + // Four instances exist but none has died, so the residue is empty and + // must report "no data" rather than 0%. + `checkd(slot_retired_cov_is_none(), 1); + + // A type name the registry has never seen. + `checkd(unknown_live(), 0); + `checkd(unknown_created(), 0); + `checkd(unknown_retired(), 0); + `checkd(unknown_cov_is_none(), 1); + + `checkalive(b); + b = null; // slot 1, not last -> d is swapped down into slot 1 + `checkalive(g); + g = null; + `checkalive(h); + h = null; + end + + 4: begin + `checkd(slot_live(), 3); + `checkd(slot_retired(), 1); + + // The fold ran before the unlink, on live items. A fold that ran after + // the free reads destroyed vectors and reports garbage or 0, not 50%. + `checkd(fold_retired(), 1); + `checkd(fold_retired_cov_x100(), 5000); + + // A never-sampled instance counts as 0%, per IEEE 1800-2023 19.11.3. + // Pinned here so a change is visible. + `checkd(none_retired(), 1); + `checkd(none_retired_cov_x100(), 0); + + e = new; // slot 3 + end + + 6: begin + `checkd(slot_live(), 4); + `checkd(slot_retired(), 1); + // d now sits in slot 1. Retiring it swaps e (slot 3) down into slot 1; + // if e's slot is not rewritten, e is the node that gets erased next. + `checkalive(d); + d = null; + end + + 8: begin + `checkd(slot_live(), 3); + `checkd(slot_retired(), 2); + + // e must still be the node it was: intact, samplable, and its own. + sample_all(e); + `checkr(e.get_inst_coverage(), 100.0); + + f = new; // slot 3 -- the entry a stale e.m_slot would point at + end + + 10: begin + `checkd(slot_live(), 4); + `checkd(slot_retired(), 2); + // With the fixup this retires e (slot 1); without it, e's stale slot 3 + // retires f. Counts match either way -- cycle 12 is what notices. + `checkalive(e); + e = null; + end + + 12: begin + `checkd(slot_live(), 3); + `checkd(slot_retired(), 3); + + // If f was erased instead of e, this reads freed storage (ASAN variant) + // and e's node is orphaned in the registry, which cycle 14 sees as a + // live count that never reaches 0. + sample_all(f); + `checkr(f.get_inst_coverage(), 100.0); + + `checkalive(a); + a = null; + `checkalive(c); + c = null; + `checkalive(f); + f = null; + end + + 14: begin + `checkd(slot_live(), 0); + `checkd(slot_retired(), 6); + // a, b, c, d never sampled (0%); e and f fully covered (100%). + `checkd(slot_retired_cov_x100(), 3333); + // ... and now it is real data, not the empty-residue sentinel. + `checkd(slot_retired_cov_is_none(), 0); + + $write("*-* All Finished *-*\n"); + $finish; + end + + default: ; + endcase + end +endmodule diff --git a/test_regress/t/t_covergroup_inst_retire_asan.py b/test_regress/t/t_covergroup_inst_retire_asan.py new file mode 100755 index 000000000..86f0dce41 --- /dev/null +++ b/test_regress/t/t_covergroup_inst_retire_asan.py @@ -0,0 +1,30 @@ +#!/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.top_filename = 't/t_covergroup_inst_retire.v' + +# A wrong erase-by-swap frees the wrong node and leaves the right one orphaned. +# The plain run catches that through the live count; this catches it as the +# use-after-free it also is, and catches the orphan as a leak at exit. +# +# AddressSanitizer only, not --runtime-debug: its -fsanitize=undefined and +# -D_GLIBCXX_DEBUG add nothing to a lifetime test and triple the runtime +# recompile. ASAN is incompatible with TSAN, which --runtime-debug would have +# made driver.py notice for us. +if test.tsan: + test.skip("ThreadSanitizer not compatible with AddressSanitizer\n") + +test.compile(verilator_flags2=["-CFLAGS -fsanitize=address -LDFLAGS -fsanitize=address"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_covergroup_multi_inst.out b/test_regress/t/t_covergroup_multi_inst.out new file mode 100644 index 000000000..b6946c2f2 --- /dev/null +++ b/test_regress/t/t_covergroup_multi_inst.out @@ -0,0 +1,6 @@ +cg_mi.cp_a.a0: 2 +cg_mi.cp_a.a1: 1 +cg_mi.cp_a.a2: 1 +cg_mi.cp_a.a3: 1 +cg_mi.cp_b.hi: 2 +cg_mi.cp_b.lo: 3 diff --git a/test_regress/t/t_covergroup_multi_inst.py b/test_regress/t/t_covergroup_multi_inst.py new file mode 100755 index 000000000..6b0f1a9e4 --- /dev/null +++ b/test_regress/t/t_covergroup_multi_inst.py @@ -0,0 +1,15 @@ +#!/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 +import coverage_covergroup_common + +test.scenarios('vlt') + +coverage_covergroup_common.run(test) diff --git a/test_regress/t/t_covergroup_multi_inst.v b/test_regress/t/t_covergroup_multi_inst.v new file mode 100644 index 000000000..fb16c95a4 --- /dev/null +++ b/test_regress/t/t_covergroup_multi_inst.v @@ -0,0 +1,67 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Matthew Ballance +// SPDX-License-Identifier: CC0-1.0 + +// Multiple instances of one covergroup type, sampled asymmetrically. Pins both +// the merged .dat report (bins summed across instances) and each instance's +// get_inst_coverage(). The covergroup deliberately has two coverpoints with +// UNEQUAL normal-bin counts: that is the only shape where a per-item weighted +// average differs from the raw covered/total sum, so it is what makes a later +// change of the instance-coverage formula observable. + +// verilog_format: off +`define stop $stop +`define checkr(gotv,expv) do if ((((gotv) - (expv)) > 0.001) || (((expv) - (gotv)) > 0.001)) begin $write("%%Error: %s:%0d: got=%f exp=%f\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +// verilog_format: on + +module t; + logic [1:0] a; + logic [1:0] b; + + // cp_a has 4 normal bins, cp_b has 2 -- unequal on purpose (see header). + covergroup cg_mi; + cp_a: coverpoint a { + bins a0 = {0}; + bins a1 = {1}; + bins a2 = {2}; + bins a3 = {3}; + } + cp_b: coverpoint b { + bins lo = {[0 : 1]}; + bins hi = {[2 : 3]}; + } + endgroup + + cg_mi inst_full = new; // every bin hit + cg_mi inst_part = new; // one bin of each coverpoint hit + cg_mi inst_none = new; // never sampled + + initial begin + // inst_full: all four a values, both b halves + for (int i = 0; i < 4; ++i) begin + a = i[1:0]; + b = i[1:0]; + inst_full.sample(); + end + + // inst_part: a0 and lo only + a = 0; + b = 0; + inst_part.sample(); + + // inst_none is never sampled. + + // Today: raw covered/total over all bins of all items. + // inst_full 6/6 -> 100.0 + // inst_part 2/6 -> 33.333 (weighted average over items would be 37.5) + // inst_none 0/6 -> 0.0 + `checkr(inst_full.get_inst_coverage(), 100.0); + `checkr(inst_part.get_inst_coverage(), 33.333); + `checkr(inst_none.get_inst_coverage(), 0.0); + + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule