Support weight in coverage (#8472)

This commit is contained in:
Marco Bartoli
2026-09-24 14:01:45 -04:00
committed by GitHub
parent 88082a4e96
commit 1aa5ab2e3a
40 changed files with 1132 additions and 283 deletions
+28
View File
@@ -305,6 +305,34 @@ public:
#endif
};
// Internals: VlFileLineDebug stores a SystemVerilog source code location. Used in
// VlCoroutineHandle for debugging purposes, and to locate covergroup weight errors.
class VlFileLineDebug final {
// MEMBERS
#ifdef VL_DEBUG
const char* m_filename = nullptr; // Filename from sources, nullptr for unlnown
int m_lineno = 0; // Line number from sources
#endif
public:
// CONSTRUCTORS
VlFileLineDebug() = default;
VlFileLineDebug(const char* filename, int lineno)
#ifdef VL_DEBUG
: m_filename{filename}
, m_lineno{lineno}
#endif
{
}
// METHODS
#ifdef VL_DEBUG
const char* filename() const { return m_filename; }
int lineno() const { return m_lineno; }
#endif
};
class VlExecutionProfilerBase;
//=========================================================================
+11
View File
@@ -28,6 +28,8 @@
#include <cstdint>
#include <string>
class VlFileLineDebug;
// Per-bin classification. A bin's kind is which set it lives in (structural),
// not a per-bin field. Only Normal feeds coverage(); the rest are recorded.
// Enumerators are 'KIND_'-prefixed because the bare LRM terms collide with
@@ -50,6 +52,9 @@ enum class VlCovBinKind : uint8_t {
/// Bounded bin count, so random access by index is the primary usage.
class VlCoverpointIf VL_NOT_FINAL {
// MEMBERS
int32_t m_weight = 1; // option.weight; never negative
public:
// CONSTRUCTORS
virtual ~VlCoverpointIf() = default;
@@ -62,6 +67,12 @@ public:
virtual std::string binName(uint32_t i) const = 0;
// Bins covered / effective total (Normal set only) for the coverage calc
virtual void coverageParts(double& covered, double& total) const = 0;
/// Weight of this item in its covergroup instance's coverage (option.weight,
/// IEEE 1800-2023 19.11)
int32_t weight() const { return m_weight; }
/// 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);
};
#endif // Guard
+107 -15
View File
@@ -1155,12 +1155,75 @@ void VlCoverCrossDyn::finalizeBins() {
//=============================================================================
// VlCovergroupInst
// IEEE 1800-2023 19.11: coverage is the weighted average of the contributions; with a zero
// denominator it is 0.0, or 100.0 when the covergroup's weight is zero
static double _vl_cov_calculate(double weighted, double weights, int32_t weight) VL_PURE {
if (weights == 0.0) return weight ? 0.0 : 100.0;
return weighted / weights;
}
// IEEE 1800-2023 19.7: a weight shall be non-negative. A negative constant is rejected when
// verilating; a weight that is negative only at run time is reported as it is loaded, and
// counts as zero, so that coverage stays within 0..100.
static int32_t _vl_cov_load_weight(const char* optionp, IData value,
VlFileLineDebug fileline) VL_MT_SAFE {
const int32_t weight = static_cast<int32_t>(value);
if (VL_LIKELY(weight >= 0)) return weight;
const char* filep = ""; // VlFileLineDebug keeps the location only under VL_DEBUG
int line = 0;
#ifdef VL_DEBUG
filep = fileline.filename();
line = fileline.lineno();
#else
static_cast<void>(fileline);
#endif
const std::string where = filep && filep[0]
? std::string{filep} + ":" + std::to_string(line) + ": "
: std::string{};
VL_PRINTF_MT("%%Error: %sCoverage option '%s' is set to negative value '%d';"
" weights must be non-negative (IEEE 1800-2023 19.7)\n",
where.c_str(), optionp, static_cast<int>(weight));
VL_STOP_MT(filep, line, "");
return 0;
}
void VlCoverpointIf::weight(uint32_t value, VlFileLineDebug fileline) {
m_weight = _vl_cov_load_weight("option.weight", value, fileline);
}
VlCoverCrossDyn* VlCovergroupInst::addCrossDyn() {
VlCoverCrossDyn* const cxp = new VlCoverCrossDyn{};
m_items.emplace_back(cxp);
return cxp;
}
void VlCovergroupInst::loadWeight() {
// Only a new value, so that each negative value is reported once
if (!m_weightp || *m_weightp == m_loadedWeight) return;
m_loadedWeight = *m_weightp;
m_weight = _vl_cov_load_weight("option.weight", m_loadedWeight, m_fileline);
}
std::pair<double, double> VlCovergroupInst::coverageSums() const {
double weighted = 0.0;
double weights = 0.0;
for (const auto& itemp : m_items) {
double covered = 0.0;
double total = 0.0;
itemp->coverageParts(covered, total);
if (total == 0.0) continue; // No bins: excluded from both sums
weighted += itemp->weight() * (covered / total);
weights += itemp->weight();
}
return {100.0 * weighted, weights};
}
double VlCovergroupInst::coverage() {
loadWeight();
const std::pair<double, double> sums = coverageSums();
return _vl_cov_calculate(sums.first, sums.second, m_weight);
}
//=============================================================================
// VlCovergroupType / VlCovRegistry
@@ -1174,19 +1237,19 @@ VlCovergroupInst* VlCovergroupType::newInstance() {
return instp;
}
void VlCovergroupType::foldResidue(const VlCovergroupInst* instp) {
double covered = 0.0;
double total = 0.0;
instp->coverageParts(covered, total);
void VlCovergroupType::foldResidue(VlCovergroupInst* instp) {
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,
// 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;
if (sums.second == 0.0) return;
// The same weighted average of the items as get_inst_coverage(), so that a live
// instance and the same instance one delta after death never disagree. With the
// weight last loaded: the object that lent option.weight is gone, and nothing may
// be reported here, as this can run after ~VerilatedContext (see ~VlCovRegistry).
const int32_t weight = instp->weight();
m_retired.sumCoverage += weight * (sums.first / sums.second);
m_retired.sumWeight += weight;
++m_retired.count;
}
@@ -1231,9 +1294,28 @@ bool VlCovergroupType::anyAttached() const {
return false;
}
double VlCovergroupType::coverage(IData typeWeight, 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);
}
// Instances that have died still count: their contribution is the residue
double sumCoverage = m_retired.sumCoverage;
double sumWeight = m_retired.sumWeight;
for (const auto& instp : m_insts) {
if (instp->retained()) continue; // Already folded into the residue
instp->loadWeight();
const std::pair<double, double> sums = instp->coverageSums();
if (sums.second == 0.0) continue; // A covergroup without coverage does not contribute
sumCoverage += instp->weight() * (sums.first / sums.second);
sumWeight += instp->weight();
}
return _vl_cov_calculate(sumCoverage, sumWeight, m_typeWeight);
}
double VlCovergroupType::retiredCoverage() const {
if (m_retired.count == 0) return -1.0;
return m_retired.sumCoverage / static_cast<double>(m_retired.count);
if (m_retired.count == 0 || m_retired.sumWeight == 0.0) return -1.0;
return m_retired.sumCoverage / m_retired.sumWeight;
}
// Defined here, not in verilated.cpp, so that the registry costs nothing in a model with no
@@ -1252,13 +1334,23 @@ VlCovRegistry* VerilatedContext::covergroupRegistryp() VL_MT_SAFE {
return static_cast<VlCovRegistry*>(m_covergroupsp.get());
}
VlCovergroupInst* VlCovRegistry::newCovergroupInst(const char* typeName) {
VlCovergroupType* VlCovRegistry::findOrCreateType(const char* typeName) {
VlCovergroupType*& typep = m_byName[typeName];
if (!typep) { // First instance of this type
if (!typep) { // First use of this type
m_types.emplace_back(new VlCovergroupType{});
typep = m_types.back().get();
}
return typep->newInstance();
return typep;
}
VlCovergroupInst* VlCovRegistry::newCovergroupInst(const char* typeName) {
return findOrCreateType(typeName)->newInstance();
}
double VlCovRegistry::typeCoverage(const char* typeName, IData typeWeight,
VlFileLineDebug fileline) {
// Also for a type never instantiated, whose node then remembers type_option.weight
return findOrCreateType(typeName)->coverage(typeWeight, fileline);
}
// A covergroup object can outlive the registry: models must be destroyed before
+62 -19
View File
@@ -41,6 +41,7 @@
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
class VerilatedCovContext;
@@ -490,6 +491,12 @@ class VlCovergroupInst final {
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
// option.weight of the SV object that created this node, while that object
// lives; borrowed through VlCovInstHandle::lendWeight().
const IData* m_weightp = nullptr;
IData m_loadedWeight = 1; // Last option.weight loaded through m_weightp
int32_t m_weight = 1; // Weight in use, never negative; kept once the object is gone
VlFileLineDebug m_fileline; // Covergroup declaration, where a negative weight is reported
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.
@@ -525,35 +532,49 @@ public:
// here, and because it frees 'this'.
bool attachDec() { return --m_attachCount == 0; }
// ---- instance weight (from VlCovInstHandle) ----
void lendWeight(const IData* weightp, VlFileLineDebug fileline) {
m_weightp = weightp;
m_fileline = fileline;
loadWeight();
}
// The lending object is being destroyed. Its members may already be gone, so
// the weight is not read again; the last loaded value stays in effect.
void unlendWeight(const IData* weightp) {
if (m_weightp == weightp) m_weightp = nullptr;
}
/// Load option.weight from the lending object. SV writes the member directly
/// (assignments, ref and output arguments, $value$plusargs, ...), so this is
/// where a new value is seen, and checked once: a negative weight is reported as
/// an error, and counts as zero.
void loadWeight();
/// Weight of this instance in its type's coverage (option.weight, IEEE
/// 1800-2023 19.11.3), as last loaded; never negative.
int32_t weight() const { return m_weight; }
// ---- 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;
}
}
/// IEEE 1800-2023 19.11 sums over the items whose coverage has a nonzero
/// 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;
/// Instance coverage, as returned by get_inst_coverage(), in 0..100.
double coverage();
};
//=============================================================================
// 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.
/// not grow with churn. Each instance contributes with its option.weight.
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
double sumCoverage = 0.0; // Sigma of per-instance weight * coverage (0..100)
double sumWeight = 0.0; // Sigma of per-instance weight
};
//=============================================================================
@@ -569,11 +590,13 @@ 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
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.
void foldResidue(const VlCovergroupInst* instp);
void foldResidue(VlCovergroupInst* instp);
public:
// CONSTRUCTORS
@@ -589,6 +612,12 @@ 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);
// ---- introspection ----
// Test and debug only; generated code never calls these, and SV reaches them
@@ -603,7 +632,8 @@ public:
uint32_t createdInstanceCount() const { return m_createdInsts; }
// Instances that have died and contributed to the residue.
uint32_t retiredInstanceCount() const { return static_cast<uint32_t>(m_retired.count); }
// Mean coverage over the retired instances only, in 0..100; -1.0 if none.
// Weighted mean coverage over the retired instances only, in 0..100; -1.0 if
// none contributed or their weights sum to zero.
double retiredCoverage() const;
};
@@ -621,6 +651,7 @@ class VlCovRegistry final : public VerilatedVirtualBase {
// PRIVATE METHODS
VlCovergroupType* findType(const char* typeName) const; // nullptr if unknown
VlCovergroupType* findOrCreateType(const char* typeName);
public:
// CONSTRUCTORS
@@ -633,6 +664,9 @@ public:
// 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);
/// 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);
// ---- introspection (see VlCovergroupType) ----
// typeName is the obfuscated generated name, so a test using these under
@@ -658,18 +692,21 @@ public:
class VlCovInstHandle final {
// MEMBERS
VlCovergroupInst* m_p = nullptr; // Attach-counted; the registry owns the node
const IData* m_weightp = nullptr; // Owning object's option.weight, if lent to m_p
// 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) {
static void release(VlCovergroupInst* instp, const IData* weightp) {
if (VL_UNCOVERABLE(!instp)) return; // Never attach()ed; codegen always does
instp->unlendWeight(weightp);
if (instp->attachDec()) instp->typep()->retire(instp);
}
public:
// CONSTRUCTORS
VlCovInstHandle() = default;
// The copy's owning object lends no weight; the node keeps reading the lender's.
VlCovInstHandle(const VlCovInstHandle& o)
: m_p{o.m_p} {
if (VL_UNCOVERABLE(!m_p)) return; // Unbound source; see release above
@@ -678,12 +715,18 @@ public:
// 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); }
~VlCovInstHandle() { release(m_p, m_weightp); }
// 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; }
// Let the node read the owning object's option.weight until this handle is
// destroyed. Called once, from the generated constructor, after attach().
void lendWeight(const IData* weightp, VlFileLineDebug fileline) {
m_weightp = weightp;
m_p->lendWeight(weightp, fileline);
}
VlCovergroupInst* p() const { return m_p; }
};
+6 -6
View File
@@ -253,7 +253,7 @@ inline bool VlClassRef<`systemc_class_name>::operator<(const VlClassRef<`systemc
// them as such currently, so named with a unique prefix
typedef struct {
string name;
int weight;
int weight = 1;
int goal;
string comment;
int at_least;
@@ -266,7 +266,7 @@ inline bool VlClassRef<`systemc_class_name>::operator<(const VlClassRef<`systemc
} vl_covergroup_options_t;
typedef struct {
int weight;
int weight = 1;
int goal;
string comment;
int at_least;
@@ -275,7 +275,7 @@ inline bool VlClassRef<`systemc_class_name>::operator<(const VlClassRef<`systemc
} vl_coverpoint_options_t;
typedef struct {
int weight;
int weight = 1;
int goal;
string comment;
int at_least;
@@ -284,7 +284,7 @@ inline bool VlClassRef<`systemc_class_name>::operator<(const VlClassRef<`systemc
} vl_cross_options_t;
typedef struct {
int weight;
int weight = 1;
int goal;
string comment;
bit strobe;
@@ -294,14 +294,14 @@ inline bool VlClassRef<`systemc_class_name>::operator<(const VlClassRef<`systemc
} vl_covergroup_type_options_t;
typedef struct {
int weight;
int weight = 1;
int goal;
string comment;
real real_interval;
} vl_coverpoint_type_options_t;
typedef struct {
int weight;
int weight = 1;
int goal;
string comment;
} vl_cross_type_options_t;
-29
View File
@@ -64,35 +64,6 @@
// Placeholder for compiling with --protect-ids
#define VL_UNKNOWN "<unknown>"
//=============================================================================
// VlFileLineDebug stores a SystemVerilog source code location. Used in VlCoroutineHandle for
// debugging purposes.
class VlFileLineDebug final {
// MEMBERS
#ifdef VL_DEBUG
const char* m_filename = nullptr; // Filename from sources, nullptr for unlnown
int m_lineno = 0; // Line number from sources
#endif
public:
// CONSTRUCTORS
VlFileLineDebug() = default;
VlFileLineDebug(const char* filename, int lineno)
#ifdef VL_DEBUG
: m_filename{filename}
, m_lineno{lineno}
#endif
{
}
// METHODS
#ifdef VL_DEBUG
const char* filename() const { return m_filename; }
int lineno() const { return m_lineno; }
#endif
};
//=============================================================================
// VlCoroutineHandle is a non-copyable (but movable) coroutine handle. On resume, the handle is
// cleared, as we assume that either the coroutine has finished and deleted itself, or, if it got