Fix coverage merge instances (#8543)

This commit is contained in:
Marco Bartoli
2026-10-06 19:12:55 -04:00
committed by GitHub
parent a89aa64ba6
commit 704ec556ea
24 changed files with 1064 additions and 79 deletions
+1
View File
@@ -422,6 +422,7 @@ detailed descriptions of these arguments.
--coverage-max-bins <value> Maximum bins of an integral coverpoint bins declaration
--coverage-max-real-bins <value> Maximum bins of a real coverpoint bins declaration
--coverage-max-width <width> Maximum array depth for coverage
--coverage-merge-instances Default covergroups to merge their instances
--coverage-per-instance Enable per-instance coverage counters
--coverage-toggle Enable toggle coverage
--coverage-underscore Enable coverage of _signals
+22
View File
@@ -349,6 +349,28 @@ Summary:
toggle coverage. Defaults to 256, as covering large vectors may greatly
slow coverage simulations.
.. option:: --coverage-merge-instances
Sets the default value of the covergroup type option
``type_option.merge_instances`` to 1, for the covergroups that do not
set it (IEEE 1800-2023 19.11.3).
With "--coverage-merge-instances",
the type coverage of a covergroup merges its instances as a union of
their bins. This applies to ``get_coverage()``, to
``get_inst_coverage()`` unless ``option.get_inst_coverage`` is set, and
to the coverage that :command:`verilator_coverage` reports. This is
like the default behavior of Synopsys VCS. In Questa, it is the
equivalent of ``vsim -cvgmergeinstances``, which sets
``SVCovergroupMergeInstancesDefault`` to 1.
With "--no-coverage-merge-instances", the default, the value is 0, as
IEEE specifies, so the type coverage of a covergroup is the average of
the coverage of its instances.
This is unrelated to :vlopt:`--coverage-per-instance`, which keeps
Verilator-inserted coverage per hierarchy instance.
.. option:: --coverage-per-instance
For Verilator-inserted coverage, preserve generated coverage counters and
+10
View File
@@ -28,6 +28,7 @@
#include <cstdint>
#include <string>
class VlCovMergedItems;
class VlFileLineDebug;
// Per-bin classification. A bin's kind is which set it lives in (structural),
@@ -54,6 +55,7 @@ enum class VlCovBinKind : uint8_t {
class VlCoverpointIf VL_NOT_FINAL {
// MEMBERS
int32_t m_weight = 1; // option.weight; never negative
int32_t m_typeWeight = 1; // type_option.weight; never negative
public:
// CONSTRUCTORS
@@ -73,6 +75,14 @@ public:
/// Load option.weight, as evaluated by the covergroup constructor. A negative
/// weight is reported as an error, and counts as zero.
void weight(uint32_t value, VlFileLineDebug fileline);
/// Weight of this item in its covergroup type's coverage when the instances are merged
/// (type_option.weight, IEEE 1800-2023 19.11.3)
int32_t typeWeight() const { return m_typeWeight; }
/// Load type_option.weight, a constant that verilation checked is not negative
void typeWeight(uint32_t value) { m_typeWeight = static_cast<int32_t>(value); }
/// Merge this item's coverable bins into its entry in 'items', the covergroup type's
/// items merged over the instances
virtual void mergeInto(VlCovMergedItems& items) const = 0;
};
#endif // Guard
+118 -6
View File
@@ -1093,6 +1093,13 @@ std::string VlCoverpoint::normalBinName(uint32_t crossIdx) const {
return declaredBinName(m_crossToBin[crossIdx]);
}
void VlCoverpoint::mergeInto(VlCovMergedItems& items) const {
VlCovMergedItem& item = items.findNewItem(m_hier);
item.options(m_atLeast, typeWeight());
// The bins coverageParts() counts
for (const uint32_t bin : m_crossToBin) item.addBin(declaredBinName(bin), m_counts[bin]);
}
const VlCovNamer& VlCoverpoint::namerFor(uint32_t i) const {
// Namers are appended in ascending order covering [0, m_total).
const auto it = std::upper_bound(
@@ -1434,6 +1441,25 @@ std::string VlCoverCross::autoBinName(uint32_t flat) const {
return name;
}
void VlCoverCross::mergeInto(VlCovMergedItems& items) const {
VlCovMergedItem& item = items.findNewItem(m_hier);
item.options(1, typeWeight()); // A cross bin is covered once hit
// The bins coverageParts() counts, which share a bin when they share its name. As no bin
// name holds ',' or '<', an automatic bin's, <bin1,...,binN> (IEEE 1800-2023 19.11.3), is
// that of no other automatic bin, nor of an explicit bin.
const uint32_t explicitCount
= hasExplicitBins() ? static_cast<uint32_t>(m_explicitp->bins.size()) : 0;
for (uint32_t i = 0; i < binCount(); ++i) {
if (i < explicitCount) {
const Bin& bin = m_explicitp->bins[i];
if (bin.kind == VlCovBinKind::KIND_NORMAL) item.addBin(bin.namep, bin.count);
continue;
}
const uint32_t flat = autoIndex(i - explicitCount);
item.addBin(autoBinName(flat), m_flatCountsp[flat]);
}
}
#if VM_COVERAGE
void VlCoverCross::registerBins(VerilatedCovContext* covcontextp, const char* page,
uint32_t itemWeight, uint32_t groupWeight) {
@@ -1793,10 +1819,83 @@ double VlCovergroupInst::coverage() {
return _vl_cov_calculate(sums.first, sums.second, m_weight);
}
void VlCovergroupInst::mergeInto(VlCovMergedItems& items) const {
for (const auto& itemp : m_items) itemp->mergeInto(items);
}
//=============================================================================
// VlCovMergedItem / VlCovMergedItems
// Call 'f' with the values that sorted maps 'a' and 'b' have for each key of either, or nullptr
// for a map without the key, so as with those of the maps merged, without merging them
template <typename T_Map, typename T_Func>
static void _vl_cov_union_walk(const T_Map& a, const T_Map& b, T_Func f) {
auto ait = a.cbegin();
auto bit = b.cbegin();
while (ait != a.cend() || bit != b.cend()) {
if (bit == b.cend() || (ait != a.cend() && ait->first < bit->first)) {
f(&ait->second, nullptr);
++ait;
} else if (ait == a.cend() || bit->first < ait->first) {
f(nullptr, &bit->second);
++bit;
} else {
f(&ait->second, &bit->second);
++ait;
++bit;
}
}
}
void VlCovMergedItem::coverageParts(double& covered, double& total) const {
uint64_t numCovered = 0;
for (const auto& it : m_counts) {
if (it.second >= m_atLeast) ++numCovered;
}
covered = static_cast<double>(numCovered);
total = static_cast<double>(m_counts.size());
}
void VlCovMergedItem::coverageParts(const VlCovMergedItem& other, double& covered,
double& total) const {
// As merged: a bin is covered once its counts sum to the larger option.at_least
const uint32_t atLeast = std::max(m_atLeast, other.m_atLeast);
uint64_t numCovered = 0;
uint64_t numTotal = 0;
_vl_cov_union_walk(m_counts, other.m_counts, [&](const uint64_t* ap, const uint64_t* bp) {
if ((ap ? *ap : 0) + (bp ? *bp : 0) >= atLeast) ++numCovered;
++numTotal;
});
covered = static_cast<double>(numCovered);
total = static_cast<double>(numTotal);
}
std::pair<double, double> VlCovMergedItems::coverageSums(const VlCovMergedItems& other) const {
double weighted = 0.0;
double weights = 0.0;
const auto sumItem = [&](const VlCovMergedItem* ap, const VlCovMergedItem* bp) {
double covered = 0.0;
double total = 0.0;
const VlCovMergedItem& itemr = ap ? *ap : *bp;
if (ap && bp) {
ap->coverageParts(*bp, covered, total);
} else {
itemr.coverageParts(covered, total);
}
if (total == 0.0) return; // No bins: excluded from both sums
weighted += itemr.typeWeight() * (covered / total);
weights += itemr.typeWeight();
};
_vl_cov_union_walk(m_items, other.m_items, sumItem);
return {100.0 * weighted, weights};
}
//=============================================================================
// VlCovergroupType / VlCovRegistry
VlCovergroupInst* VlCovergroupType::newInstance() {
VlCovergroupInst* VlCovergroupType::newInstance(bool mayMerge) {
// Once set, kept: the bins of the instances that die from now on are kept
m_mayMerge = m_mayMerge || mayMerge;
VlCovergroupInst* const instp = new VlCovergroupInst{this, m_nextInstId++};
m_insts.emplace_back(instp);
#if !VM_COVERAGE
@@ -1807,6 +1906,8 @@ VlCovergroupInst* VlCovergroupType::newInstance() {
}
void VlCovergroupType::foldResidue(VlCovergroupInst* instp) {
// Merged type coverage needs the dead instance's bins themselves
if (m_mayMerge) instp->mergeInto(m_mergedRetired);
const std::pair<double, double> sums = instp->coverageSums();
// Nothing coverable: excluded from both sums, so it moves neither the mean
// nor the denominator. Never-sampled is different: it has bins, none hit,
@@ -1863,11 +1964,22 @@ bool VlCovergroupType::anyAttached() const {
return false;
}
double VlCovergroupType::coverage(IData typeWeight, VlFileLineDebug fileline) {
double VlCovergroupType::coverage(IData typeWeight, bool mergeInstances,
VlFileLineDebug fileline) {
if (typeWeight != m_loadedTypeWeight) { // Only a new value, as in loadWeight()
m_loadedTypeWeight = typeWeight;
m_typeWeight = _vl_cov_load_weight("type_option.weight", typeWeight, fileline);
}
if (mergeInstances) {
// The union of the bins of every instance, including those that have died, kept in
// m_mergedRetired, so merged with the live ones' as read, not copied
VlCovMergedItems live;
for (const auto& instp : m_insts) {
if (!instp->retained()) instp->mergeInto(live); // Retained: in m_mergedRetired
}
const std::pair<double, double> sums = live.coverageSums(m_mergedRetired);
return _vl_cov_calculate(sums.first, sums.second, m_typeWeight);
}
// Instances that have died still count: their contribution is the residue
double sumCoverage = m_retired.sumCoverage;
double sumWeight = m_retired.sumWeight;
@@ -1912,14 +2024,14 @@ VlCovergroupType* VlCovRegistry::findOrCreateType(const char* typeName) {
return typep;
}
VlCovergroupInst* VlCovRegistry::newCovergroupInst(const char* typeName) {
return findOrCreateType(typeName)->newInstance();
VlCovergroupInst* VlCovRegistry::newCovergroupInst(const char* typeName, bool mayMerge) {
return findOrCreateType(typeName)->newInstance(mayMerge);
}
double VlCovRegistry::typeCoverage(const char* typeName, IData typeWeight,
double VlCovRegistry::typeCoverage(const char* typeName, IData typeWeight, bool mergeInstances,
VlFileLineDebug fileline) {
// Also for a type never instantiated, whose node then remembers type_option.weight
return findOrCreateType(typeName)->coverage(typeWeight, fileline);
return findOrCreateType(typeName)->coverage(typeWeight, mergeInstances, fileline);
}
// A covergroup object can outlive the registry: models must be destroyed before
+73 -13
View File
@@ -35,9 +35,11 @@
#include "verilated.h"
#include "verilated_cov_model.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <initializer_list>
#include <map>
#include <memory>
#include <string>
#include <unordered_map>
@@ -258,6 +260,7 @@ public:
covered = numCovered;
total = m_normal;
}
void mergeInto(VlCovMergedItems& items) const override;
};
//=============================================================================
@@ -460,6 +463,7 @@ public:
total = hasExplicitBins() ? m_explicitp->normalBins + m_explicitp->autoBins.size()
: m_numAutoBins;
}
void mergeInto(VlCovMergedItems& items) const override;
};
//=============================================================================
@@ -632,6 +636,8 @@ public:
/// denominator: {the sum of each item's option.weight times its coverage
/// (0..100), the sum of those weights}.
std::pair<double, double> coverageSums() const;
/// Merge the items into 'items', the covergroup type's items merged over the instances
void mergeInto(VlCovMergedItems& items) const;
/// Instance coverage, as returned by get_inst_coverage(), in 0..100.
double coverage();
};
@@ -647,6 +653,52 @@ struct VlCovRetiredAvg final {
double sumWeight = 0.0; // Sigma of per-instance weight
};
//=============================================================================
// VlCovMergedItem
/// A coverpoint or cross merged over the instances of its covergroup type: the union of their
/// coverable bins, which share a bin when they share its name, with the counts summed (IEEE
/// 1800-2023 19.11.3, type_option.merge_instances true).
class VlCovMergedItem final {
// MEMBERS
uint32_t m_atLeast = 0; // The largest option.at_least of the instances
int32_t m_typeWeight = 1; // Its type_option.weight
std::map<std::string, uint64_t> m_counts; // Coverable bin name -> count, summed
public:
// METHODS
/// Merge the options of an instance: a merged bin is covered once hit the largest
/// option.at_least of the instances (IEEE 1800-2023 19.11.1); type_option.weight is the
/// same for every instance
void options(uint32_t atLeast, int32_t typeWeight) {
m_atLeast = std::max(m_atLeast, atLeast);
m_typeWeight = typeWeight;
}
void addBin(const std::string& name, uint64_t count) { m_counts[name] += count; }
int32_t typeWeight() const { return m_typeWeight; }
/// Merged bins that reached option.at_least, and all merged bins
void coverageParts(double& covered, double& total) const;
/// As coverageParts(), of this and 'other' merged
void coverageParts(const VlCovMergedItem& other, double& covered, double& total) const;
};
//=============================================================================
// VlCovMergedItems
/// The items of a covergroup type merged over its instances, by item name.
class VlCovMergedItems final {
// MEMBERS
std::map<std::string, VlCovMergedItem> m_items; // By "covergroup.item" name
public:
// METHODS
VlCovMergedItem& findNewItem(const std::string& name) { return m_items[name]; }
/// Sums as VlCovergroupInst::coverageSums(), of these items and those of 'other' merged,
/// with each item weighted by its type_option.weight (IEEE 1800-2023 19.7.1). Without
/// merging them into a copy, as 'other', those of the instances that have died, may be large.
std::pair<double, double> coverageSums(const VlCovMergedItems& other) const;
};
//=============================================================================
// VlCovergroupType
/// One covergroup type: owns its live instances, in creation order, plus the
@@ -660,12 +712,15 @@ class VlCovergroupType final {
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
// The bins of every instance that has died, kept only if the type may merge its instances
VlCovMergedItems m_mergedRetired;
bool m_mayMerge = false; // type_option.merge_instances is, or may become, true
IData m_loadedTypeWeight = 1; // Last type_option.weight loaded by coverage()
int32_t m_typeWeight = 1; // type_option.weight in use, never negative
// PRIVATE METHODS
// Harvest instp's contribution into m_retired. Must run before instp is
// unlinked: it reads the instance's items.
// Harvest instp's contribution into m_retired, and m_mergedRetired. Must run before
// instp is unlinked: it reads the instance's items.
void foldResidue(VlCovergroupInst* instp);
public:
@@ -674,7 +729,8 @@ public:
VL_UNCOPYABLE(VlCovergroupType);
// METHODS
VlCovergroupInst* newInstance();
// mayMerge: type_option.merge_instances is, or may become, true
VlCovergroupInst* newInstance(bool mayMerge);
// 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.
@@ -682,12 +738,13 @@ public:
// 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;
/// Type coverage, as returned by get_coverage(), in 0..100: the average of
/// every instance's coverage, weighted by its option.weight (IEEE 1800-2023
/// 19.11.3, type_option.merge_instances false). typeWeight is
/// type_option.weight, which decides the result when no instance contributes;
/// like option.weight, it is checked as it is loaded.
double coverage(IData typeWeight, VlFileLineDebug fileline);
/// Type coverage, as returned by get_coverage(), in 0..100 (IEEE 1800-2023 19.11.3).
/// Unless mergeInstances (type_option.merge_instances), the average of every
/// instance's coverage, weighted by its option.weight; else the coverage of the union
/// of the instances' bins, with each item weighted by its type_option.weight.
/// typeWeight is type_option.weight, which decides the result when no instance or item
/// contributes; like option.weight, it is checked as it is loaded.
double coverage(IData typeWeight, bool mergeInstances, VlFileLineDebug fileline);
// ---- introspection ----
// Test and debug only; generated code never calls these, and SV reaches them
@@ -733,11 +790,14 @@ public:
// Find-or-create the type node, then add an instance to it. typeName is the
// covergroup type's name, as $typename names it (e.g. "pkg::cls::cg"), already
// --protect-ids obfuscated, and is the same string that keys the coverage
// database's hier/page.
VlCovergroupInst* newCovergroupInst(const char* typeName);
// database's hier/page. mayMerge: type_option.merge_instances is, or may
// become, true.
VlCovergroupInst* newCovergroupInst(const char* typeName, bool mayMerge);
/// Type coverage of a covergroup type (get_coverage()); see
/// VlCovergroupType::coverage(). typeWeight is its type_option.weight.
double typeCoverage(const char* typeName, IData typeWeight, VlFileLineDebug fileline);
/// VlCovergroupType::coverage(). typeWeight is its type_option.weight, and
/// mergeInstances its type_option.merge_instances.
double typeCoverage(const char* typeName, IData typeWeight, bool mergeInstances,
VlFileLineDebug fileline);
// ---- introspection (see VlCovergroupType) ----
// typeName is the obfuscated generated name, so a test using these under
+2 -1
View File
@@ -904,7 +904,8 @@ inline std::ostream& operator<<(std::ostream& os, const VBranchPred& rhs) {
macro(COVERGROUP_SIZED_RANGE_W, "sizedRangeW", false, "rr") \
macro(COVERGROUP_SIZED_SAMPLE, "sizedSample", false, "rrr") \
macro(COVERGROUP_SIZED_SAMPLE_W, "sizedSampleW", false, "rrr") \
macro(COVERGROUP_TYPE_COVERAGE, "typeCoverage", false, "rrr") \
macro(COVERGROUP_TYPE_COVERAGE, "typeCoverage", false, "rrrr") \
macro(COVERGROUP_TYPE_WEIGHT, "typeWeight", false, "r") \
macro(COVERGROUP_VALUE_EXCLUDED, "valueExcluded", PURE, "r") \
macro(COVERGROUP_VALUE_EXCLUDED_W, "valueExcludedW", PURE, "r") \
macro(COVERGROUP_VALUE_FINALIZE, "valueFinalize", false, "") \
+128 -46
View File
@@ -43,12 +43,14 @@
VL_DEFINE_DEBUG_FUNCTIONS;
//######################################################################
// Embedded covergroup assignment validation
// Embedded covergroup assignment validation; and in the same pass over the netlist, the
// type_option variables through which SystemVerilog may set type_option.merge_instances
class CovergroupAssignValidVisitor final : public VNVisitorConst {
VMemberMap m_memberMap;
std::map<const AstVar*, const AstNodeFTask*>
m_constructors; // Implicit instance -> constructor
std::set<const AstVar*> m_mergeableTypeOptions; // See mergeableTypeOptions()
const AstNodeFTask* m_ftaskp = nullptr;
bool m_collecting = true;
bool m_valid = true;
@@ -96,6 +98,19 @@ class CovergroupAssignValidVisitor final : public VNVisitorConst {
}
iterateChildrenConst(nodep);
}
void visit(AstNodeVarRef* nodep) override {
// Type options may be set at any time (IEEE 1800-2023 19.7.1), so SystemVerilog may set
// type_option.merge_instances with any use of a type_option but a read of another
// member, as not every write is marked yet (std::randomize)
if (m_collecting && nodep->varp()->name() == "type_option") {
const AstStructSel* const selp = VN_CAST(nodep->backp(), StructSel);
if (!selp || selp->fromp() != nodep || selp->name() == "merge_instances"
|| !nodep->access().isReadOnly()) {
m_mergeableTypeOptions.insert(nodep->varp());
}
}
iterateChildrenConst(nodep);
}
void visit(AstNode* nodep) override { iterateChildrenConst(nodep); }
public:
@@ -106,6 +121,9 @@ public:
if (!m_constructors.empty()) iterateConst(nodep);
}
bool valid() const { return m_valid; }
// type_option variables through which SystemVerilog may set type_option.merge_instances, so
// that covergroups are tested with optionVar(true)
const std::set<const AstVar*>& mergeableTypeOptions() const { return m_mergeableTypeOptions; }
};
//######################################################################
@@ -248,8 +266,14 @@ class FunctionalCoverageVisitor final : public VNVisitor {
std::vector<AstCoverpoint*> m_coverpoints; // Coverpoints in current covergroup
std::map<std::string, AstCoverpoint*> m_coverpointMap; // Name -> coverpoint for fast lookup
std::vector<AstCoverCross*> m_coverCrosses; // Cross coverage items in current covergroup
std::vector<AstCgOptionAssign*> m_cgOptions; // Covergroup-level weights, before lowering
std::vector<AstCgOptionAssign*> m_cgOptions; // Covergroup-level options, before lowering
uint32_t m_cgTypeWeight = 1; // The covergroup's type_option.weight, a constant
bool m_cgMergeInstances = false; // The covergroup's type_option.merge_instances, a constant
// The covergroup's type_option.merge_instances is, or SystemVerilog may make it, true
bool m_cgMayMerge = false;
// type_option members through which SystemVerilog may set type_option.merge_instances,
// see CovergroupAssignValidVisitor::mergeableTypeOptions()
const std::set<const AstVar*>& m_mergeableTypeOptions;
struct EmbeddedEventTrigger final {
FileLine* eventFl; // Clocking-event source location
@@ -323,32 +347,47 @@ class FunctionalCoverageVisitor final : public VNVisitor {
return varp;
}
// 'option.weight' or 'type_option.weight', per optionVarp
AstStructSel* newWeightSel(FileLine* fl, AstVar* optionVarp, VAccess access) {
const AstMemberDType* const memberp = VN_AS(
m_memberMap.findMember(optionVarp->dtypep()->skipRefp(), "weight"), MemberDType);
UASSERT_OBJ(memberp, optionVarp, "Coverage option structure missing 'weight'");
// 'option.<member>' or 'type_option.<member>', per optionVarp
AstStructSel* newOptionSel(FileLine* fl, AstVar* optionVarp, const std::string& member,
VAccess access) {
const AstMemberDType* const memberp
= VN_AS(m_memberMap.findMember(optionVarp->dtypep()->skipRefp(), member), MemberDType);
UASSERT_OBJ(memberp, optionVarp, "Coverage option structure missing '" << member << "'");
AstNodeExpr* const fromp = optionVarp->lifetime().isStatic()
? new AstVarRef{fl, optionVarp, access}
: memberRef(fl, optionVarp, access);
AstStructSel* const selp = new AstStructSel{fl, fromp, "weight"};
AstStructSel* const selp = new AstStructSel{fl, fromp, member};
selp->dtypep(memberp->subDTypep()->skipRefToEnump());
selp->didWidth(true);
return selp;
}
// Store the covergroup-level weights (IEEE 1800-2023 19.7) where SystemVerilog and the
// runtime read them. option.weight is evaluated by the constructor, as are the other
// instance options; type_option.weight is constant, and initializes the static member.
// Store the covergroup-level options (IEEE 1800-2023 19.7) where SystemVerilog and the
// runtime read them. The instance options, option.weight and option.get_inst_coverage,
// are evaluated by the constructor, as are the other instance options; the type options,
// type_option.weight and type_option.merge_instances, are constant, and initialize the
// static member. Without its own type_option.merge_instances, a covergroup has the
// default of --coverage-merge-instances.
void lowerCovergroupOptions() {
bool mergeSet = false; // The covergroup sets type_option.merge_instances
for (AstCgOptionAssign* const optp : m_cgOptions) {
UASSERT_OBJ(optp->optType() == VCoverOptionType::WEIGHT, optp,
"Unexpected covergroup option reaching V3Covergroup");
FileLine* const fl = optp->fileline();
// V3Width left type_option.weight a non-negative constant
if (optp->typeOption()) m_cgTypeWeight = VN_AS(optp->valuep(), Const)->toUInt();
// V3Width left the type options constant, and type_option.weight non-negative
const AstConst* const constp = VN_CAST(optp->valuep(), Const);
std::string member = "weight";
if (optp->optType() == VCoverOptionType::MERGE_INSTANCES) {
member = "merge_instances";
m_cgMergeInstances = !constp->num().isEqZero();
mergeSet = true;
} else if (optp->optType() == VCoverOptionType::GET_INST_COVERAGE) {
member = "get_inst_coverage";
} else {
UASSERT_OBJ(optp->optType() == VCoverOptionType::WEIGHT, optp,
"Unexpected covergroup option reaching V3Covergroup");
if (optp->typeOption()) m_cgTypeWeight = constp->toUInt();
}
AstAssign* const assignp = new AstAssign{
fl, newWeightSel(fl, optionVar(optp->typeOption()), VAccess::WRITE),
fl, newOptionSel(fl, optionVar(optp->typeOption()), member, VAccess::WRITE),
optp->valuep()->unlinkFrBack()};
if (optp->typeOption()) {
m_covergroupp->addMembersp(new AstInitialStatic{fl, assignp});
@@ -359,12 +398,21 @@ class FunctionalCoverageVisitor final : public VNVisitor {
}
}
m_cgOptions.clear();
if (!mergeSet && v3Global.opt.coverageMergeInstances()) {
FileLine* const fl = m_covergroupp->fileline();
m_covergroupp->addMembersp(new AstInitialStatic{
fl, new AstAssign{
fl, newOptionSel(fl, optionVar(true), "merge_instances", VAccess::WRITE),
new AstConst{fl, AstConst::BitTrue{}}}});
m_cgMergeInstances = true;
}
m_cgMayMerge = m_cgMergeInstances || m_mergeableTypeOptions.count(optionVar(true));
}
// The weight of an item in the coverage database, which merges the instances: its
// option.weight if a constant, and so of every instance; else its type_option.weight, the
// weight of type coverage merged over the instances (IEEE 1800-2023 19.7.1)
static uint32_t itemDatabaseWeight(AstNode* optionsp) {
// type_option.weight if the covergroup merges them too (IEEE 1800-2023 19.11.3); else its
// option.weight if a constant, and so of every instance; else its type_option.weight
uint32_t itemDatabaseWeight(AstNode* optionsp) const {
const AstNodeExpr* weightp = nullptr; // The option.weight in effect
uint32_t typeWeight = 1;
for (AstNode* nodep = optionsp; nodep; nodep = nodep->nextp()) {
@@ -377,22 +425,30 @@ class FunctionalCoverageVisitor final : public VNVisitor {
weightp = optp->valuep();
}
}
if (m_cgMergeInstances) return typeWeight;
if (!weightp) return 1;
if (const AstConst* const constp = VN_CAST(weightp, Const)) return constp->toUInt();
return typeWeight;
}
// Configure an item's option.weight, its weight in instance coverage (IEEE 1800-2023
// 19.11). type_option.weight only weighs type coverage merged over the instances, which
// type_option.merge_instances would select; without that, it has no effect.
// 19.11), and, if the covergroup may merge its instances, its type_option.weight, its
// weight in type coverage then (19.11.3).
void generateItemWeight(FileLine* fl, AstVar* itemVarp, AstNode* optionsp) {
for (AstNode* nodep = optionsp; nodep; nodep = nodep->nextp()) {
const AstCoverOption* const optp = VN_AS(nodep, CoverOption);
if (!(optp->optType() == VCoverOptionType::WEIGHT) || optp->typeOption()) continue;
m_constructorp->addStmtsp(
itemCall(fl, itemVarp, VCMethod::COVERGROUP_WEIGHT,
{optp->valuep()->cloneTree(false), fileLineDebug(optp->fileline())})
->makeStmt());
if (!(optp->optType() == VCoverOptionType::WEIGHT)) continue;
if (!optp->typeOption()) {
m_constructorp->addStmtsp(
itemCall(fl, itemVarp, VCMethod::COVERGROUP_WEIGHT,
{optp->valuep()->cloneTree(false), fileLineDebug(optp->fileline())})
->makeStmt());
} else if (m_cgMayMerge) {
// V3Width left type_option.weight a non-negative constant
m_constructorp->addStmtsp(itemCall(fl, itemVarp, VCMethod::COVERGROUP_TYPE_WEIGHT,
{optp->valuep()->cloneTree(false)})
->makeStmt());
}
}
}
@@ -412,6 +468,8 @@ class FunctionalCoverageVisitor final : public VNVisitor {
m_droppedCrosses.clear();
m_cgInstVarp = nullptr;
m_cgTypeWeight = 1;
m_cgMergeInstances = false;
m_cgMayMerge = false;
lowerCovergroupOptions();
@@ -1067,12 +1125,13 @@ class FunctionalCoverageVisitor final : public VNVisitor {
itemCall(fl, m_cgInstVarp, VCMethod::COVERGROUP_ATTACH,
{ctext(fl, "vlSymsp->_vm_contextp__->covergroupRegistryp()"
"->newCovergroupInst("
+ quoted(covergroupProtectedName()) + ")")},
+ quoted(covergroupProtectedName())
+ (m_cgMayMerge ? ", true" : ", false") + ")")},
/*usePtr=*/false)
->makeStmt());
// The node reads option.weight in place, so procedural assignments take effect
AstCExpr* const weightAddrp = new AstCExpr{fl, "&"};
weightAddrp->add(newWeightSel(fl, optionVar(false), VAccess::READ));
weightAddrp->add(newOptionSel(fl, optionVar(false), "weight", VAccess::READ));
m_constructorp->addStmtsp(itemCall(fl, m_cgInstVarp, VCMethod::COVERGROUP_LEND_WEIGHT,
{weightAddrp, fileLineDebug(fl)}, /*usePtr=*/false)
->makeStmt());
@@ -3924,34 +3983,50 @@ class FunctionalCoverageVisitor final : public VNVisitor {
m_memberMap.clear();
// get_inst_coverage(): the average of the coverpoints and crosses, weighted by their
// option.weight (IEEE 1800-2023 19.11). The instance node holds their runtimes.
// option.weight (IEEE 1800-2023 19.11). The instance node holds their runtimes. With
// the instances merged, get_coverage() instead, unless option.get_inst_coverage
// (Table 19-1).
AstFunc* const getInstCoveragep
= VN_AS(m_memberMap.findMember(m_covergroupp, "get_inst_coverage"), Func);
FileLine* const instFl = getInstCoveragep->fileline();
AstCMethodHard* const instCallp = instanceCall(instFl, VCMethod::COVERGROUP_COVERAGE);
instCallp->dtypeSetDouble();
AstNodeExpr* instValuep = instCallp;
if (m_cgMayMerge) {
AstNodeExpr* const mergedp = new AstLogAnd{
instFl, newOptionSel(instFl, optionVar(true), "merge_instances", VAccess::READ),
new AstLogNot{instFl, newOptionSel(instFl, optionVar(false), "get_inst_coverage",
VAccess::READ)}};
instValuep = new AstCond{instFl, mergedp, typeCoverageCall(instFl), instCallp};
}
getInstCoveragep->addStmtsp(new AstAssign{
instFl, new AstVarRef{instFl, VN_AS(getInstCoveragep->fvarp(), Var), VAccess::WRITE},
instCallp});
instValuep});
// get_coverage(): the average of the covergroup's instances, weighted by their
// option.weight (IEEE 1800-2023 19.11.3). Static, so the registry finds the instances.
// option.weight, or with type_option.merge_instances, the coverage of the union of their
// bins (IEEE 1800-2023 19.11.3). Static, so the registry finds the instances.
AstFunc* const getCoveragep
= VN_AS(m_memberMap.findMember(m_covergroupp, "get_coverage"), Func);
FileLine* const typeFl = getCoveragep->fileline();
AstCExpr* const registryp
= ctext(typeFl, "vlSymsp->_vm_contextp__->covergroupRegistryp()");
registryp->dtypeSetVoid(); // Opaque receiver; only ever the 'fromp' of the call below
AstCMethodHard* const typeCallp
= new AstCMethodHard{typeFl, registryp, VCMethod::COVERGROUP_TYPE_COVERAGE};
typeCallp->addPinsp(ctext(typeFl, quoted(covergroupProtectedName())));
typeCallp->addPinsp(newWeightSel(typeFl, optionVar(true), VAccess::READ));
typeCallp->addPinsp(fileLineDebug(m_covergroupp->fileline()));
typeCallp->usePtr(true);
typeCallp->dtypeSetDouble();
getCoveragep->addStmtsp(new AstAssign{
typeFl, new AstVarRef{typeFl, VN_AS(getCoveragep->fvarp(), Var), VAccess::WRITE},
typeCallp});
typeCoverageCall(typeFl)});
}
// The registry call computing the covergroup's type coverage, per its type options
AstCMethodHard* typeCoverageCall(FileLine* fl) {
AstCExpr* const registryp = ctext(fl, "vlSymsp->_vm_contextp__->covergroupRegistryp()");
registryp->dtypeSetVoid(); // Opaque receiver; only ever the 'fromp' of the call below
AstCMethodHard* const callp
= new AstCMethodHard{fl, registryp, VCMethod::COVERGROUP_TYPE_COVERAGE};
callp->addPinsp(ctext(fl, quoted(covergroupProtectedName())));
callp->addPinsp(newOptionSel(fl, optionVar(true), "weight", VAccess::READ));
callp->addPinsp(newOptionSel(fl, optionVar(true), "merge_instances", VAccess::READ));
callp->addPinsp(fileLineDebug(m_covergroupp->fileline()));
callp->usePtr(true);
callp->dtypeSetDouble();
return callp;
}
// VISITORS
@@ -4505,7 +4580,7 @@ class FunctionalCoverageVisitor final : public VNVisitor {
iterateChildren(nodep);
}
// V3Width leaves only the covergroup-level weights, for lowerCovergroupOptions()
// V3Width leaves only the covergroup-level options lowerCovergroupOptions() stores
void visit(AstCgOptionAssign* nodep) override { m_cgOptions.push_back(nodep); }
// A package, interface, or module, so the design unit declaring the classes within
@@ -4519,7 +4594,11 @@ class FunctionalCoverageVisitor final : public VNVisitor {
public:
// CONSTRUCTORS
explicit FunctionalCoverageVisitor(AstNetlist* nodep) { iterate(nodep); }
FunctionalCoverageVisitor(AstNetlist* nodep,
const std::set<const AstVar*>& mergeableTypeOptions)
: m_mergeableTypeOptions{mergeableTypeOptions} {
iterate(nodep);
}
~FunctionalCoverageVisitor() override = default;
};
@@ -4528,7 +4607,10 @@ public:
void V3Covergroup::covergroup(AstNetlist* nodep) {
UINFO(4, __FUNCTION__ << ": ");
if (!CovergroupAssignValidVisitor{nodep}.valid()) V3Error::abortIfErrors();
{ FunctionalCoverageVisitor{nodep}; } // Destruct before checking
const CovergroupAssignValidVisitor validVisitor{nodep};
if (!validVisitor.valid()) V3Error::abortIfErrors();
{ // Destruct before checking
FunctionalCoverageVisitor{nodep, validVisitor.mergeableTypeOptions()};
}
V3Global::dumpCheckGlobalTree("coveragefunc", 0, dumpTreeEitherLevel() >= 3);
}
+1
View File
@@ -1393,6 +1393,7 @@ void V3Options::parseOptsList(FileLine* fl, const string& optdir, int argc,
parseBinsLimit("--coverage-max-real-bins", valp, m_coverageMaxRealBins);
});
DECL_OPTION("-coverage-max-width", Set, &m_coverageMaxWidth);
DECL_OPTION("-coverage-merge-instances", OnOff, &m_coverageMergeInstances);
DECL_OPTION("-coverage-per-instance", OnOff, &m_coveragePerInstance);
DECL_OPTION("-coverage-toggle", OnOff, &m_coverageToggle);
DECL_OPTION("-coverage-underscore", OnOff, &m_coverageUnderscore);
+2
View File
@@ -233,6 +233,7 @@ private:
bool m_coverageExpr = false; // main switch: --coverage-expr
bool m_coverageFsm = false; // main switch: --coverage-fsm
bool m_coverageLine = false; // main switch: --coverage-block
bool m_coverageMergeInstances = false; // main switch: --coverage-merge-instances
bool m_coveragePerInstance = false; // main switch: --coverage-per-instance
bool m_coverageToggle = false; // main switch: --coverage-toggle
bool m_coverageUnderscore = false; // main switch: --coverage-underscore
@@ -533,6 +534,7 @@ public:
bool coverageExpr() const { return m_coverageExpr; }
bool coverageFsm() const { return m_coverageFsm; }
bool coverageLine() const { return m_coverageLine; }
bool coverageMergeInstances() const { return m_coverageMergeInstances; }
bool coveragePerInstance() const { return m_coveragePerInstance; }
bool coverageToggle() const { return m_coverageToggle; }
bool coverageUnderscore() const { return m_coverageUnderscore; }
+15 -6
View File
@@ -2162,13 +2162,22 @@ class WidthVisitor final : public VNVisitor {
"'option.auto_bin_max'; using default value");
}
} else if (nodep->optType() == VCoverOptionType::MERGE_INSTANCES) {
// get_coverage() always averages the instances (IEEE 1800-2023 19.11.3)
const AstConst* const constp = VN_CAST(nodep->valuep(), Const);
if (!constp || !constp->num().isEqZero()) {
nodep->v3warn(COVERIGN, "Ignoring unsupported: 'type_option.merge_instances';"
" type coverage is the weighted average of the"
" instances");
// V3Covergroup stores it into type_option; as a type option it is constant
// (IEEE 1800-2023 19.7.1)
iterateCheckBool(nodep, "merge_instances", nodep->valuep(), BOTH);
V3Const::constifyEdit(nodep->valuep());
AstNodeExpr* const valuep = nodep->valuep();
if (!VN_IS(valuep, Const)) {
valuep->v3error("Coverage option 'type_option.merge_instances' requires a"
" constant expression (IEEE 1800-2023 19.7.1)");
valuep->replaceWith(new AstConst{valuep->fileline(), AstConst::BitFalse{}});
VL_DO_DANGLING(pushDeletep(valuep), valuep);
}
return;
} else if (nodep->optType() == VCoverOptionType::GET_INST_COVERAGE) {
// V3Covergroup stores it into option, as the constructor evaluates it
iterateCheckBool(nodep, "get_inst_coverage", nodep->valuep(), BOTH);
return;
} else if (nodep->optType() == VCoverOptionType::DISTRIBUTE_FIRST) {
// A bins 'with' filter applies before the values are distributed to the bins
// (IEEE 1800-2023 19.5.1.1)
+1 -1
View File
@@ -56,7 +56,7 @@ int main(int argc, char* argv[]) {
{
// ---- Case 1: attach count above 1 ----
VlCovInstHandle first;
first.attach(registryp->newCovergroupInst("cg_handle"));
first.attach(registryp->newCovergroupInst("cg_handle", false));
checkEq("one handle, one node", registryp->liveInstanceCount("cg_handle"), 1);
checkEq("nothing retired yet", registryp->retiredInstanceCount("cg_handle"), 0);
@@ -0,0 +1,95 @@
t.cg_avg.checked.auto_0: 1
t.cg_avg.checked.auto_1: 1
t.cg_avg.helper.auto_0: 2
t.cg_avg.helper.auto_1: 0
t.cg_cross.cp_a.auto_0: 1
t.cg_cross.cp_a.auto_1: 0
t.cg_cross.cp_b.auto_0: 1
t.cg_cross.cp_b.auto_1: 0
t.cg_cross.cp_b.auto_2: 0
t.cg_cross.cp_b.auto_3: 0
t.cg_cross.x.<auto_0,auto_0> [cross]: 1
t.cg_cross.x.<auto_0,auto_1> [cross]: 0
t.cg_cross.x.<auto_0,auto_2> [cross]: 0
t.cg_cross.x.<auto_0,auto_3> [cross]: 0
t.cg_cross.x.<auto_1,auto_0> [cross]: 0
t.cg_cross.x.<auto_1,auto_1> [cross]: 0
t.cg_cross.x.<auto_1,auto_2> [cross]: 0
t.cg_cross.x.<auto_1,auto_3> [cross]: 0
t.cg_cross_ignore.cp_a.auto_0: 1
t.cg_cross_ignore.cp_a.auto_1: 0
t.cg_cross_ignore.cp_b.auto_0: 1
t.cg_cross_ignore.cp_b.auto_1: 0
t.cg_cross_ignore.x.<auto_0,auto_0> [cross]: 1
t.cg_cross_ignore.x.<auto_0,auto_1> [cross]: 0
t.cg_cross_ignore.x.skip [ignore,cross]: 0
t.cg_cross_joined.p.a: 1
t.cg_cross_joined.p.a_x_b: 0
t.cg_cross_joined.q.b_x_c: 1
t.cg_cross_joined.q.c: 0
t.cg_cross_joined.xy.<a,b_x_c> [cross]: 1
t.cg_cross_joined.xy.<a,c> [cross]: 0
t.cg_cross_joined.xy.<a_x_b,b_x_c> [cross]: 0
t.cg_cross_joined.xy.<a_x_b,c> [cross]: 0
t.cg_cross_named.cp_a.auto_0: 1
t.cg_cross_named.cp_a.auto_1: 0
t.cg_cross_named.cp_b.auto_0: 1
t.cg_cross_named.cp_b.auto_1: 0
t.cg_cross_named.x.<auto_0,auto_1> [cross]: 0
t.cg_cross_named.x.<auto_1,auto_0> [cross]: 0
t.cg_cross_named.x.<auto_1,auto_1> [cross]: 0
t.cg_cross_named.x.auto_1_x_auto_1 [cross]: 1
t.cg_cross_opt.cp_a.auto_0: 1
t.cg_cross_opt.cp_a.auto_1: 0
t.cg_cross_opt.cp_b.auto_0: 1
t.cg_cross_opt.cp_b.auto_1: 0
t.cg_cross_opt.cp_b.auto_2: 0
t.cg_cross_opt.cp_b.auto_3: 0
t.cg_cross_opt.x.<auto_0,auto_0> [cross]: 1
t.cg_cross_opt.x.<auto_0,auto_1> [cross]: 0
t.cg_cross_opt.x.<auto_0,auto_2> [cross]: 0
t.cg_cross_opt.x.<auto_0,auto_3> [cross]: 0
t.cg_cross_opt.x.<auto_1,auto_0> [cross]: 0
t.cg_cross_opt.x.<auto_1,auto_1> [cross]: 0
t.cg_cross_opt.x.<auto_1,auto_2> [cross]: 0
t.cg_cross_opt.x.<auto_1,auto_3> [cross]: 0
t.cg_cross_union.cp_a.auto_0: 1
t.cg_cross_union.cp_a.auto_1: 1
t.cg_cross_union.cp_b.auto_0: 1
t.cg_cross_union.cp_b.auto_1: 1
t.cg_cross_union.x.<auto_0,auto_0> [cross]: 1
t.cg_cross_union.x.<auto_0,auto_1> [cross]: 0
t.cg_cross_union.x.<auto_1,auto_0> [cross]: 0
t.cg_cross_union.x.<auto_1,auto_1> [cross]: 1
t.cg_dead.cp.auto_0: 1
t.cg_dead.cp.auto_1: 1
t.cg_dead_union.a.b[0]: 1
t.cg_dead_union.a.b[1]: 1
t.cg_dead_union.a.b[2]: 1
t.cg_dead_union.a.b[3]: 0
t.cg_inst.checked.auto_0: 1
t.cg_inst.checked.auto_1: 1
t.cg_inst.helper.auto_0: 2
t.cg_inst.helper.auto_1: 0
t.cg_merged.checked.auto_0: 1
t.cg_merged.checked.auto_1: 1
t.cg_merged.helper.auto_0: 2
t.cg_merged.helper.auto_1: 0
t.cg_opt.checked.auto_0: 1
t.cg_opt.checked.auto_1: 1
t.cg_opt.helper.auto_0: 2
t.cg_opt.helper.auto_1: 0
t.cg_proc.cp.auto_0: 1
t.cg_proc.cp.auto_1: 1
t.cg_sum.cp.auto_0: 2
t.cg_sum.cp.auto_1: 0
t.cg_union.a.b[0]: 1
t.cg_union.a.b[1]: 2
t.cg_union.a.b[2]: 0
t.cg_union_avg.a.b[0]: 1
t.cg_union_avg.a.b[1]: 2
t.cg_union_avg.a.b[2]: 0
t.cg_zero.cp.auto_0: 1
t.cg_zero.cp.auto_1: 0
t.cg_zero_type.cp.auto_0: 1
t.cg_zero_type.cp.auto_1: 0
+15
View File
@@ -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_all')
coverage_covergroup_common.run(test)
@@ -0,0 +1,303 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Test type_option.merge_instances (IEEE 1800-2023 19.7.1, 19.11.3)
// 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 checkp(gotv,expv) do if ($sformatf("%0.2f", gotv) != (expv)) begin $write("%%Error: %s:%0d: got=%0.2f exp=%s\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0);
// verilog_format: on
module t (
input clk
);
int cyc = 0;
// Without merge_instances, type coverage averages the instances, whose items weigh by
// option.weight, so helper's type_option.weight has no effect: 75
covergroup cg_avg with function sample (bit a, bit b);
helper: coverpoint a {
type_option.weight = 0;
}
checked: coverpoint b;
endgroup
// Merged, the items weigh by type_option.weight: 100, and get_inst_coverage() returns
// get_coverage() (Table 19-1)
covergroup cg_merged with function sample (bit a, bit b);
type_option.merge_instances = 1;
helper: coverpoint a {
type_option.weight = 0;
}
checked: coverpoint b;
endgroup
// With option.get_inst_coverage, get_inst_coverage() is the instance's coverage: 75
covergroup cg_inst with function sample (bit a, bit b);
type_option.merge_instances = 1;
option.get_inst_coverage = 1;
helper: coverpoint a {
type_option.weight = 0;
}
checked: coverpoint b;
endgroup
// Merged, option.weight has no effect: 75, and the instance's coverage: 100
covergroup cg_opt with function sample (bit a, bit b);
type_option.merge_instances = 1;
option.get_inst_coverage = 1;
helper: coverpoint a {
option.weight = 0;
}
checked: coverpoint b;
endgroup
// As the example of 19.11.3: bins b[0], b[1] and b[1], b[2] merge by name, 2 of 3 covered
covergroup cg_union(int l, h) with function sample (int a);
type_option.merge_instances = 1;
option.get_inst_coverage = 1;
coverpoint a {
bins b[] = {[0 : 3]} with (item >= l && item <= h);
}
endgroup
// The same averaged: 100 and 50
covergroup cg_union_avg(int l, h) with function sample (int a);
coverpoint a {
bins b[] = {[0 : 3]} with (item >= l && item <= h);
}
endgroup
// The counts of a merged bin sum: at_least 2, and each instance hits auto[0] once: 50
covergroup cg_sum with function sample (bit a);
type_option.merge_instances = 1;
cp: coverpoint a {
option.at_least = 2;
}
endgroup
// A cross weighs by type_option.weight: (50 + 25 + 3 * 12.5) / 5
covergroup cg_cross with function sample (bit a, bit [1:0] b);
type_option.merge_instances = 1;
cp_a: coverpoint a;
cp_b: coverpoint b;
x: cross cp_a, cp_b{type_option.weight = 3;}
endgroup
// And not by option.weight: (50 + 25 + 12.5) / 3
covergroup cg_cross_opt with function sample (bit a, bit [1:0] b);
type_option.merge_instances = 1;
cp_a: coverpoint a;
cp_b: coverpoint b;
x: cross cp_a, cp_b{option.weight = 3;}
endgroup
// Cross bins merge by name: (100 + 100 + 50) / 3
covergroup cg_cross_union with function sample (bit a, bit b);
type_option.merge_instances = 1;
cp_a: coverpoint a;
cp_b: coverpoint b;
x: cross cp_a, cp_b;
endgroup
// An explicit cross bin named as an automatic bin's bins joined with _x_ stays apart from it:
// 1 of 4 covered
covergroup cg_cross_named with function sample (bit a, bit b);
type_option.merge_instances = 1;
cp_a: coverpoint a {
type_option.weight = 0;
}
cp_b: coverpoint b {
type_option.weight = 0;
}
x: cross cp_a, cp_b{
bins auto_1_x_auto_1 = binsof (cp_a) intersect {0} && binsof (cp_b) intersect {0};
}
endgroup
// Automatic cross bins stay apart though their bins' names joined with _x_ collide, <a,b_x_c>
// and <a_x_b,c>: 1 of 4 covered
covergroup cg_cross_joined with function sample (bit x, bit y);
type_option.merge_instances = 1;
p: coverpoint x {
type_option.weight = 0;
bins a = {0};
bins a_x_b = {1};
}
q: coverpoint y {
type_option.weight = 0;
bins b_x_c = {0};
bins c = {1};
}
xy: cross p, q;
endgroup
// Ignored cross bins are not coverable: 1 of 2 covered
covergroup cg_cross_ignore with function sample (bit a, bit b);
type_option.merge_instances = 1;
cp_a: coverpoint a {
type_option.weight = 0;
}
cp_b: coverpoint b {
type_option.weight = 0;
}
x: cross cp_a, cp_b{ignore_bins skip = binsof (cp_a) intersect {1};}
endgroup
// Every item of type weight zero: 0, or 100 if the covergroup's type weight is zero too
covergroup cg_zero with function sample (bit a);
type_option.merge_instances = 1;
cp: coverpoint a {
type_option.weight = 0;
}
endgroup
covergroup cg_zero_type with function sample (bit a);
type_option.merge_instances = 1;
type_option.weight = 0;
cp: coverpoint a {
type_option.weight = 0;
}
endgroup
// The bins of an instance that has died still count: 100
covergroup cg_dead with function sample (bit a);
type_option.merge_instances = 1;
cp: coverpoint a;
endgroup
// The bins of an instance that has died still count, though no live instance has them: b[0]
// and b[1] hit by the instance that dies, b[2] and b[3] of the live one, 3 of 4 covered
covergroup cg_dead_union(int l, h) with function sample (int a);
type_option.merge_instances = 1;
coverpoint a {
bins b[] = {[0 : 3]} with (item >= l && item <= h);
}
endgroup
// Merged by an assignment during simulation (IEEE 1800-2023 19.7.1): 50 averaged, and 100
// merged, with the bins of an instance that died before
covergroup cg_proc with function sample (bit a);
cp: coverpoint a;
endgroup
cg_avg avg = new;
cg_merged merged = new;
cg_inst inst = new;
cg_opt opt = new;
cg_union union_1 = new(0, 1);
cg_union union_2 = new(1, 2);
cg_union_avg union_avg_1 = new(0, 1);
cg_union_avg union_avg_2 = new(1, 2);
cg_sum sum_1 = new;
cg_sum sum_2 = new;
cg_cross cross_w = new;
cg_cross_opt cross_opt = new;
cg_cross_union cross_union_1 = new;
cg_cross_union cross_union_2 = new;
cg_cross_named cross_named = new;
cg_cross_joined cross_joined = new;
cg_cross_ignore cross_ignore = new;
cg_zero zero = new;
cg_zero_type zero_type = new;
cg_dead dead_1 = new;
cg_dead dead_2 = new;
cg_dead_union dead_union_1 = new(2, 3);
cg_dead_union dead_union_2 = new(0, 1);
cg_proc proc_1 = new;
cg_proc proc_2 = new;
initial begin
avg.sample(0, 0);
avg.sample(0, 1);
`checkp(avg.get_coverage(), "75.00");
`checkp(avg.get_inst_coverage(), "75.00");
merged.sample(0, 0);
merged.sample(0, 1);
`checkp(merged.get_coverage(), "100.00");
`checkp(merged.get_inst_coverage(), "100.00");
inst.sample(0, 0);
inst.sample(0, 1);
`checkp(inst.get_coverage(), "100.00");
`checkp(inst.get_inst_coverage(), "75.00");
opt.sample(0, 0);
opt.sample(0, 1);
`checkp(opt.get_coverage(), "75.00");
`checkp(opt.get_inst_coverage(), "100.00");
union_1.sample(0);
union_1.sample(1);
union_2.sample(1);
`checkp(union_1.get_coverage(), "66.67");
`checkp(union_1.get_inst_coverage(), "100.00");
`checkp(union_2.get_inst_coverage(), "50.00");
union_avg_1.sample(0);
union_avg_1.sample(1);
union_avg_2.sample(1);
`checkp(union_avg_1.get_coverage(), "75.00");
sum_1.sample(0);
sum_2.sample(0);
`checkp(sum_1.get_coverage(), "50.00");
`checkp(sum_1.get_inst_coverage(), "50.00");
cross_w.sample(0, 0);
`checkp(cross_w.get_coverage(), "22.50");
cross_opt.sample(0, 0);
`checkp(cross_opt.get_coverage(), "29.17");
cross_union_1.sample(0, 0);
cross_union_2.sample(1, 1);
`checkp(cross_union_1.get_coverage(), "83.33");
cross_named.sample(0, 0);
`checkp(cross_named.get_coverage(), "25.00");
cross_joined.sample(0, 0);
`checkp(cross_joined.get_coverage(), "25.00");
cross_ignore.sample(0, 0);
`checkp(cross_ignore.get_coverage(), "50.00");
zero.sample(0);
`checkp(zero.get_coverage(), "0.00");
zero_type.sample(0);
`checkp(zero_type.get_coverage(), "100.00");
dead_1.sample(0);
dead_2.sample(1);
dead_union_1.sample(2);
dead_union_2.sample(0);
dead_union_2.sample(1);
proc_1.sample(0);
proc_2.sample(1);
end
// Instances die once their evaluation ends, so after a clock edge
function int retired_dead();
retired_dead = $c32(
"Verilated::threadContextp()->covergroupRegistryp()->retiredInstanceCount(\"t.cg_dead\")");
endfunction
function int retired_proc();
retired_proc = $c32(
"Verilated::threadContextp()->covergroupRegistryp()->retiredInstanceCount(\"t.cg_proc\")");
endfunction
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 1) begin
dead_2 = null;
dead_union_2 = null;
proc_2 = null;
end
else if (cyc == 3) begin
`checkd(retired_dead(), 1);
`checkp(dead_1.get_coverage(), "100.00");
`checkp(dead_union_1.get_coverage(), "75.00");
`checkd(retired_proc(), 1);
`checkp(proc_1.get_coverage(), "50.00");
`checkp(proc_1.get_inst_coverage(), "50.00");
cg_proc::type_option.merge_instances = 1;
`checkp(proc_1.get_coverage(), "100.00");
`checkp(proc_1.get_inst_coverage(), "100.00");
proc_1.type_option.merge_instances = 0;
`checkp(proc_1.get_coverage(), "50.00");
$write("*-* All Finished *-*\n");
$finish;
end
end
endmodule
@@ -0,0 +1,6 @@
%Error: t/t_covergroup_merge_instances_bad.v:13:35: Coverage option 'type_option.merge_instances' requires a constant expression (IEEE 1800-2023 19.7.1)
: ... note: In instance 't'
13 | type_option.merge_instances = merge;
| ^~~~~
... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance.
%Error: Exiting due to
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of either the GNU Lesser General Public License Version 3
# or the Perl Artistic License Version 2.0.
# SPDX-FileCopyrightText: 2026 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('vlt')
test.lint(fails=True, expect_filename=test.golden_filename)
test.passes()
@@ -0,0 +1,18 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// type_option.merge_instances is a type option, so constant (IEEE 1800-2023 19.7.1)
module t;
bit a;
covergroup cg(bit merge);
type_option.merge_instances = merge; // <--- Bad: not constant
cp: coverpoint a;
endgroup
cg c = new(1);
endmodule
@@ -0,0 +1,11 @@
$ verilator_coverage --report hierarchy --levels 1 coverage.dat
Covergroup Coverage Summary:
t : 93.8% (21/26)
t.C::cg_emb : 100.0% ( 2/ 2)
t.cg_default : 100.0% ( 3/ 4)
t.cg_inst : 100.0% ( 3/ 4)
t.cg_multi : 100.0% ( 2/ 2)
t.cg_multi_off : 100.0% ( 2/ 2)
t.cg_off : 75.0% ( 3/ 4)
t.cg_on : 100.0% ( 3/ 4)
t.cg_optw : 75.0% ( 3/ 4)
+32
View File
@@ -0,0 +1,32 @@
#!/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
from coverage_common import init_log, run_vlcov, vlcov_run_context
test.scenarios('vlt')
test.compile(verilator_flags2=['--coverage-user', '--coverage-merge-instances'])
test.execute()
# The coverage the report computes: as get_coverage() does, except for cg_multi_off, whose
# instances the coverage database merges
log = test.obj_dir + "/vlcov.log"
tmp_log = test.obj_dir + "/vlcov.tmp"
init_log(log)
vlcov = vlcov_run_context(test, log, tmp_log)
run_vlcov(vlcov,
"verilator_coverage --report hierarchy --levels 1 coverage.dat",
args=["--report", "hierarchy", "--levels", "1", test.obj_dir + "/coverage.dat"])
test.files_identical(log, test.golden_filename)
test.passes()
@@ -0,0 +1,134 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Test --coverage-merge-instances, the default of type_option.merge_instances (IEEE 1800-2023
// 19.11.3)
// 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 checkp(gotv,expv) do if ($sformatf("%0.2f", gotv) != (expv)) begin $write("%%Error: %s:%0d: got=%0.2f exp=%s\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0);
// verilog_format: on
module t;
// Merged by default, the items weigh by type_option.weight: 100
covergroup cg_default with function sample (bit a, bit b);
helper: coverpoint a {
type_option.weight = 0;
}
checked: coverpoint b;
endgroup
// Its own type_option.merge_instances wins: 75
covergroup cg_off with function sample (bit a, bit b);
type_option.merge_instances = 0;
helper: coverpoint a {
type_option.weight = 0;
}
checked: coverpoint b;
endgroup
covergroup cg_on with function sample (bit a, bit b);
type_option.merge_instances = 1;
helper: coverpoint a {
type_option.weight = 0;
}
checked: coverpoint b;
endgroup
// get_inst_coverage() is the instance's coverage only with option.get_inst_coverage: 75
covergroup cg_inst with function sample (bit a, bit b);
option.get_inst_coverage = 1;
helper: coverpoint a {
type_option.weight = 0;
}
checked: coverpoint b;
endgroup
// Merged, option.weight has no effect: 75
covergroup cg_optw with function sample (bit a, bit b);
helper: coverpoint a {
option.weight = 0;
}
checked: coverpoint b;
endgroup
// Two instances hitting different bins: 100 merged, and 50 averaged
covergroup cg_multi with function sample (bit a);
cp: coverpoint a;
endgroup
covergroup cg_multi_off with function sample (bit a);
type_option.merge_instances = 0;
cp: coverpoint a;
endgroup
// An embedded covergroup: the objects hit different bins, 100 merged
class C;
bit v;
covergroup cg_emb;
cp: coverpoint v;
endgroup
function new();
cg_emb = new;
endfunction
endclass
cg_default d = new;
cg_off off = new;
cg_on on = new;
cg_inst inst = new;
cg_optw optw = new;
cg_multi m1 = new;
cg_multi m2 = new;
cg_multi_off mo1 = new;
cg_multi_off mo2 = new;
C c1 = new;
C c2 = new;
initial begin
d.sample(0, 0);
d.sample(0, 1);
off.sample(0, 0);
off.sample(0, 1);
on.sample(0, 0);
on.sample(0, 1);
inst.sample(0, 0);
inst.sample(0, 1);
optw.sample(0, 0);
optw.sample(0, 1);
m1.sample(0);
m2.sample(1);
mo1.sample(0);
mo2.sample(1);
c1.v = 0;
c1.cg_emb.sample();
c2.v = 1;
c2.cg_emb.sample();
`checkd(cg_default::type_option.merge_instances, 1'b1);
`checkd(d.type_option.merge_instances, 1'b1);
`checkd(cg_off::type_option.merge_instances, 1'b0);
`checkd(cg_on::type_option.merge_instances, 1'b1);
`checkp(d.get_coverage(), "100.00");
`checkp(d.get_inst_coverage(), "100.00");
`checkp(off.get_coverage(), "75.00");
`checkp(off.get_inst_coverage(), "75.00");
`checkp(on.get_coverage(), "100.00");
`checkp(on.get_inst_coverage(), "100.00");
`checkp(inst.get_coverage(), "100.00");
`checkp(inst.get_inst_coverage(), "75.00");
`checkp(optw.get_coverage(), "75.00");
`checkp(optw.get_inst_coverage(), "75.00");
`checkp(m1.get_coverage(), "100.00");
`checkp(m1.get_inst_coverage(), "100.00");
`checkp(m2.get_inst_coverage(), "100.00");
`checkp(mo1.get_coverage(), "50.00");
`checkp(mo1.get_inst_coverage(), "50.00");
`checkp(mo2.get_inst_coverage(), "50.00");
`checkp(c1.cg_emb.get_coverage(), "100.00");
`checkp(c1.cg_emb.get_inst_coverage(), "100.00");
`checkp(c2.cg_emb.get_inst_coverage(), "100.00");
$write("*-* All Finished *-*\n");
$finish;
end
endmodule
+20
View File
@@ -0,0 +1,20 @@
#!/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')
test.top_filename = 't/t_covergroup_merge_instances.v'
# The last setting applies, so the IEEE default: instances averaged unless merged explicitly
test.compile(verilator_flags2=['--coverage-merge-instances', '--no-coverage-merge-instances'])
test.execute()
test.passes()
@@ -141,10 +141,6 @@
%Warning-COVERIGN: t/t_covergroup_unsup.v:171:7: Unsupported: explicit coverage cross bins
171 | illegal_bins lib_cross = binsof(a) with (a);
| ^~~~~~~~~~~~
%Warning-COVERIGN: t/t_covergroup_unsup.v:46:5: Ignoring unsupported: 'type_option.merge_instances'; type coverage is the weighted average of the instances
: ... note: In instance 't'
46 | type_option.merge_instances = 1;
| ^~~~~~~~~~~
%Warning-COVERIGN: t/t_covergroup_unsup.v:47:5: Ignoring unsupported: 'type_option.distribute_first'; 'with' filters apply before values are distributed to bins
: ... note: In instance 't'
47 | type_option.distribute_first = 1;
+16 -2
View File
@@ -6,14 +6,14 @@ Coverage Summary:
expr : 0.0% ( 0/ 0)
fsm_state : 0.0% ( 0/ 0)
fsm_arc : 0.0% ( 0/ 0)
covergroup : 63.7% (34/57)
covergroup : 66.1% (40/65)
Covergroup Coverage Summary:
spec#(0.5,"a.\"(b%22") : 50.0% ( 1/ 2)
spec#(0.5,"a.\"(b%22").cg : 50.0% ( 1/ 2)
spec#(0.5,"a.\"(b%22").cg.cp : 50.0% ( 1/ 2)
spec#(0.5,"a.\"(b%22").cg.cp.auto_0 : 0.0% ( 0/ 1)
spec#(0.5,"a.\"(b%22").cg.cp.auto_1 : 100.0% ( 1/ 1)
t : 64.5% (33/55)
t : 66.9% (39/63)
t.First::twin : 100.0% ( 2/ 2)
t.First::twin.cp : 100.0% ( 2/ 2)
t.First::twin.cp.hit : 100.0% ( 1/ 1)
@@ -70,6 +70,13 @@ Covergroup Coverage Summary:
t.heavy.cp : 50.0% ( 1/ 2)
t.heavy.cp.auto_0 : 100.0% ( 1/ 1)
t.heavy.cp.auto_1 : 0.0% ( 0/ 1)
t.inst_merged : 75.0% ( 3/ 4)
t.inst_merged.cp_a : 100.0% ( 2/ 2)
t.inst_merged.cp_a.auto_0 : 100.0% ( 1/ 1)
t.inst_merged.cp_a.auto_1 : 100.0% ( 1/ 1)
t.inst_merged.cp_b : 50.0% ( 1/ 2)
t.inst_merged.cp_b.auto_0 : 100.0% ( 1/ 1)
t.inst_merged.cp_b.auto_1 : 0.0% ( 0/ 1)
t.instance_weight : 100.0% ( 3/ 4)
t.instance_weight.cp_a : 50.0% ( 1/ 2)
t.instance_weight.cp_a.auto_0 : 100.0% ( 1/ 1)
@@ -85,6 +92,13 @@ Covergroup Coverage Summary:
t.threshold.cp : 50.0% ( 1/ 2)
t.threshold.cp.auto_0 : 100.0% ( 1/ 1)
t.threshold.cp.auto_1 : 0.0% ( 0/ 1)
t.type_merged : 100.0% ( 3/ 4)
t.type_merged.cp_a : 50.0% ( 1/ 2)
t.type_merged.cp_a.auto_0 : 100.0% ( 1/ 1)
t.type_merged.cp_a.auto_1 : 0.0% ( 0/ 1)
t.type_merged.cp_b : 100.0% ( 2/ 2)
t.type_merged.cp_b.auto_0 : 100.0% ( 1/ 1)
t.type_merged.cp_b.auto_1 : 100.0% ( 1/ 1)
t.type_weight : 75.0% ( 3/ 4)
t.type_weight.cp_a : 50.0% ( 1/ 2)
t.type_weight.cp_a.auto_0 : 100.0% ( 1/ 1)
@@ -61,6 +61,22 @@ module t;
}
cp_b: coverpoint b;
endgroup
// Merging the instances, type_option.weight weighs the coverpoints, as in get_coverage(): 100
covergroup type_merged with function sample (bit a, bit b);
type_option.merge_instances = 1;
cp_a: coverpoint a {
type_option.weight = 0;
}
cp_b: coverpoint b;
endgroup
// And option.weight does not; instances hitting distinct bins merge as in get_coverage(): 75
covergroup inst_merged with function sample (bit a, bit b);
type_option.merge_instances = 1;
cp_a: coverpoint a {
option.weight = 0;
}
cp_b: coverpoint b;
endgroup
// A bin is covered once hit option.at_least times: 50
covergroup threshold with function sample (bit a);
cp: coverpoint a {
@@ -176,6 +192,9 @@ module t;
weighted weighted_inst = new;
instance_weight instance_weight_inst = new;
type_weight type_weight_inst = new;
type_merged type_merged_inst = new;
inst_merged inst_merged_first = new;
inst_merged inst_merged_second = new;
threshold threshold_inst = new;
crossed crossed_inst = new;
empty empty_inst = new;
@@ -199,6 +218,10 @@ module t;
instance_weight_inst.sample(0, 1);
type_weight_inst.sample(0, 0);
type_weight_inst.sample(0, 1);
type_merged_inst.sample(0, 0);
type_merged_inst.sample(0, 1);
inst_merged_first.sample(0, 0);
inst_merged_second.sample(1, 0);
threshold_inst.sample(0);
threshold_inst.sample(0);
threshold_inst.sample(1);
@@ -227,6 +250,8 @@ module t;
`checkr(weighted_inst.get_coverage(), 100.0);
`checkr(instance_weight_inst.get_coverage(), 100.0);
`checkr(type_weight_inst.get_coverage(), 75.0);
`checkr(type_merged_inst.get_coverage(), 100.0);
`checkr(inst_merged_first.get_coverage(), 75.0);
`checkr(threshold_inst.get_coverage(), 50.0);
`checkr(crossed_inst.get_coverage(), 30.0);
`checkr(empty_inst.get_coverage(), 0.0);