Support covergroup runtime model Phase A1 (#7728)

This commit is contained in:
Matthew Ballance 2026-06-12 08:40:48 -07:00 committed by GitHub
parent 5831cc8d46
commit e03fa6c783
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 720 additions and 69 deletions

View File

@ -0,0 +1,63 @@
// -*- mode: C++; c-file-style: "cc-mode" -*-
//=============================================================================
//
// Code available from: https://verilator.org
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of either the GNU Lesser General Public License Version 3
// or the Perl Artistic License Version 2.0.
// SPDX-FileCopyrightText: 2024-2026 Wilson Snyder
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
//
//=============================================================================
///
/// \file
/// \brief Verilated functional-coverage model interfaces
///
/// Defines interface classes to runtime covergroup coverage-collection classes.
/// These are used to query coverage achievement at runtime, and (future)
/// when writing coverage to the coverage database.
///
//=============================================================================
#ifndef VERILATOR_VERILATED_COV_MODEL_H_
#define VERILATOR_VERILATED_COV_MODEL_H_
#include "verilatedos.h"
#include <cstdint>
#include <string>
// 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
// <windows.h> macros (e.g. IGNORE), which the preprocessor would expand.
enum class VlCovBinKind : uint8_t {
KIND_NORMAL = 0, // Base coverage-collecting bin
KIND_DEFAULT = 1, // Bin declared with 'default' range (which is excluded per LRM)
KIND_IGNORE = 2, // Ignore bin
KIND_ILLEGAL = 3 // Illegal bin
};
//=============================================================================
// VlCoverpointIf
/// Read-side view of a coverpoint. The writer queries bins by index; the
/// implementor computes names/kinds on demand. Bounded bin count, so random
/// access by index is the primary usage.
class VlCoverpointIf VL_NOT_FINAL {
public:
// CONSTRUCTORS
virtual ~VlCoverpointIf() = default;
// METHODS
// All bins, across every set; index range [0, binCount())
virtual int binCount() const = 0;
// Bin name in declaration order (e.g. "myBin" or "b[3]")
virtual std::string binName(int i) const = 0;
virtual VlCovBinKind binKind(int i) const = 0;
// Bins covered / effective total (Normal set only) for the coverage calc
virtual void coverageParts(double& covered, double& total) const = 0;
};
#endif // Guard

View File

@ -0,0 +1,78 @@
// -*- mode: C++; c-file-style: "cc-mode" -*-
//=============================================================================
//
// Code available from: https://verilator.org
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of either the GNU Lesser General Public License Version 3
// or the Perl Artistic License Version 2.0.
// SPDX-FileCopyrightText: 2024-2026 Wilson Snyder
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
//
//=============================================================================
///
/// \file
/// \brief Verilated functional-coverage collection runtime implementation
///
/// Compiled and linked when "verilator --coverage" is used with covergroups.
///
//=============================================================================
#include "verilatedos.h"
#include "verilated_covergroup.h"
#include "verilated_cov.h"
void VlCoverpoint::init(const char* hier, uint32_t atLeast, int nBins) {
m_hier = hier;
m_atLeast = atLeast;
m_total = nBins;
m_counts.assign(nBins, 0);
}
void VlCoverpoint::addNamer(VlCovBinKind set, int count, VlCovBinNaming naming, const char* name,
const char* file, int line, int col) {
m_namers.emplace_back(set, count, m_nextBase, naming, name, file, line, col);
m_nextBase += count;
if (set == VlCovBinKind::KIND_NORMAL) m_normal += count;
}
const VlCovNamer& VlCoverpoint::namerFor(int i) const {
// Namers are appended in ascending, contiguous index order covering [0, m_total),
// and i is always a valid bin index, so the matching namer always exists.
for (const VlCovNamer& nm : m_namers) {
if (i < nm.base() + nm.count()) return nm;
}
VL_UNREACHABLE;
}
std::string VlCoverpoint::binName(int i) const {
const VlCovNamer& nm = namerFor(i);
std::string name = nm.name();
if (nm.naming() == VlCovBinNaming::Array) name += '[' + std::to_string(i - nm.base()) + ']';
return name;
}
void VlCoverpoint::registerBins(VerilatedCovContext* covcontextp, const char* page) {
for (int i = 0; i < binCount(); ++i) {
const VlCovNamer& nm = namerFor(i);
const VlCovBinKind kind = binKind(i);
const std::string binp = binName(i);
const std::string full = m_hier + "." + binp;
const std::string lineStr = std::to_string(nm.line());
const std::string colStr = std::to_string(nm.col());
if (kind == VlCovBinKind::KIND_NORMAL) {
VL_COVER_INSERT(covcontextp, full.c_str(), &m_counts[i], "page", page, "filename",
nm.file(), "lineno", lineStr.c_str(), "column", colStr.c_str(), "bin",
binp.c_str());
} else {
const char* const binType = kind == VlCovBinKind::KIND_IGNORE ? "ignore"
: kind == VlCovBinKind::KIND_ILLEGAL ? "illegal"
: "default";
VL_COVER_INSERT(covcontextp, full.c_str(), &m_counts[i], "page", page, "filename",
nm.file(), "lineno", lineStr.c_str(), "column", colStr.c_str(), "bin",
binp.c_str(), "bin_type", binType);
}
}
}

View File

@ -0,0 +1,144 @@
// -*- mode: C++; c-file-style: "cc-mode" -*-
//=============================================================================
//
// Code available from: https://verilator.org
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of either the GNU Lesser General Public License Version 3
// or the Perl Artistic License Version 2.0.
// SPDX-FileCopyrightText: 2024-2026 Wilson Snyder
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
//
//=============================================================================
///
/// \file
/// \brief Verilated functional-coverage collection runtime
///
/// VlCoverpoint owns per-instance bin-count storage for one coverpoint,
/// computes coverage, builds bin names on demand, and registers bins with the
/// coverage database. It implements the VlCoverpointIf read interface.
///
/// Generated covergroup code holds one VlCoverpoint per coverpoint, configures
/// it in the constructor (init + add*Namer), increments bins from sample(),
/// and registers via registerBins().
///
//=============================================================================
#ifndef VERILATOR_VERILATED_COVERGROUP_H_
#define VERILATOR_VERILATED_COVERGROUP_H_
#include "verilatedos.h"
#include "verilated_cov_model.h"
#include <cstdint>
#include <string>
#include <vector>
class VerilatedCovContext;
// How a namer builds the names of the bins it covers.
enum class VlCovBinNaming : uint8_t {
Single, // "<name>" one bin
Array, // "<name>[i]" bins b[N] value array
};
// Specifies the naming scheme for a range of bins, allowing the
// specific name to be computed on-demand.
// All name strings are borrowed literals from the generated code.
class VlCovNamer final {
// MEMBERS
VlCovBinKind m_set; // which set the bins belong to
int m_count; // bins this namer covers (1 for Single)
int m_base; // first bin index (declaration order), assigned on append
VlCovBinNaming m_naming; // how bin names are built
const char* m_name; // bin name (Single) or array base name (Array)
const char* m_file; // declaration file
int m_line; // declaration line
int m_col; // declaration column
public:
// CONSTRUCTORS
VlCovNamer(VlCovBinKind set, int count, int base, VlCovBinNaming naming, const char* name,
const char* file, int line, int col)
: m_set{set}
, m_count{count}
, m_base{base}
, m_naming{naming}
, m_name{name}
, m_file{file}
, m_line{line}
, m_col{col} {}
// METHODS
VlCovBinKind set() const { return m_set; }
int count() const { return m_count; }
int base() const { return m_base; }
VlCovBinNaming naming() const { return m_naming; }
const char* name() const { return m_name; }
const char* file() const { return m_file; }
int line() const { return m_line; }
int col() const { return m_col; }
};
//=============================================================================
// VlCoverpoint
/// Per-instance coverpoint runtime. Bins are stored in declaration order; a
/// bin's set/name come from the owning namer. coverage() is computed on demand
/// by scanning bin counts, keeping the sample() hot path a plain counter bump.
class VlCoverpoint final : public VlCoverpointIf {
// MEMBERS
std::string m_hier; // "covergroup.coverpoint"
uint32_t m_atLeast = 1; // option.at_least (coverpoint-wide)
int m_total = 0; // bins across all sets
int m_normal = 0; // Normal bins (coverage denominator)
int m_nextBase = 0; // running append cursor
std::vector<uint32_t> m_counts; // [m_total], one per bin
std::vector<VlCovNamer> m_namers; // appended in declaration order
// PRIVATE METHODS
const VlCovNamer& namerFor(int i) const; // obtain the bin-specific name producer
void addNamer(VlCovBinKind set, int count, VlCovBinNaming naming, const char* name,
const char* file, int line, int col);
public:
// CONSTRUCTORS
VlCoverpoint() = default;
// METHODS
// ---- configuration (from generated constructor) ----
void init(const char* hier, uint32_t atLeast, int nBins);
void addSingleNamer(VlCovBinKind set, const char* name, const char* file, int line, int col) {
addNamer(set, 1, VlCovBinNaming::Single, name, file, line, col);
}
void addArrayNamer(VlCovBinKind set, int count, const char* name, const char* file, int line,
int col) {
addNamer(set, count, VlCovBinNaming::Array, name, file, line, col);
}
void registerBins(VerilatedCovContext* covcontextp, const char* page);
// ---- hot path (from generated sample()) ----
void incrementBin(int i) { ++m_counts[i]; } // Normal bin: count only
void recordHit(int i) { ++m_counts[i]; } // Ignore/Illegal/Default: count only
// ---- VlCoverpointIf ----
int binCount() const override { return m_total; }
std::string binName(int i) const override;
VlCovBinKind binKind(int i) const override { return namerFor(i).set(); }
void coverageParts(double& covered, double& total) const override {
// Count Normal bins that reached option.at_least on demand, so the hot
// path (incrementBin) stays a plain counter bump.
int numCovered = 0;
for (const VlCovNamer& nm : m_namers) {
if (nm.set() != VlCovBinKind::KIND_NORMAL) continue;
for (int i = nm.base(); i < nm.base() + nm.count(); ++i) {
if (m_counts[i] >= m_atLeast) ++numCovered;
}
}
covered = numCovered;
total = m_normal;
}
};
#endif // Guard

View File

@ -1191,6 +1191,19 @@ public:
= {"user", "array", "auto", "ignore", "illegal", "default", "wildcard", "transition"};
return names[m_e];
}
// VlCovBinKind enumerator naming the bin's set
const char* binSetEnum() const {
switch (m_e) {
case BINS_IGNORE: return "VlCovBinKind::KIND_IGNORE";
case BINS_ILLEGAL: return "VlCovBinKind::KIND_ILLEGAL";
case BINS_DEFAULT: return "VlCovBinKind::KIND_DEFAULT";
default: return "VlCovBinKind::KIND_NORMAL";
}
}
// Normal bins (feed coverage) are anything but ignore/illegal/default
bool binIsNormal() const {
return m_e != BINS_IGNORE && m_e != BINS_ILLEGAL && m_e != BINS_DEFAULT;
}
};
constexpr bool operator==(const VCoverBinsType& lhs, VCoverBinsType::en rhs) {
return lhs.m_e == rhs;

View File

@ -68,6 +68,9 @@ class FunctionalCoverageVisitor final : public VNVisitor {
, crossBins{cb} {}
};
std::vector<BinInfo> m_binInfos; // All bins in current covergroup
std::set<std::string> m_crossedCpNames; // Coverpoints referenced by a cross (kept legacy)
std::vector<AstVar*> m_convCpVars; // VlCoverpoint members of converted coverpoints
AstCDType* m_vlCoverpointDTypep = nullptr; // Shared "VlCoverpoint" C++ member type
VMemberMap m_memberMap; // Member names cached for fast lookup
@ -88,6 +91,17 @@ class FunctionalCoverageVisitor final : public VNVisitor {
// Clear bin info for this covergroup (deleting any orphaned cross pseudo-bins)
clearBinInfos();
// Coverpoints referenced by a cross keep the legacy per-bin-member path (the cross
// reads those members); collect their names before they are consumed by the cross.
m_crossedCpNames.clear();
m_convCpVars.clear();
for (AstCoverCross* crossp : m_coverCrosses) {
for (AstNode* itemp = crossp->itemsp(); itemp; itemp = itemp->nextp()) {
if (const AstCoverpointRef* const refp = VN_CAST(itemp, CoverpointRef))
m_crossedCpNames.insert(refp->name());
}
}
// For each coverpoint, generate sampling code
for (AstCoverpoint* cpp : m_coverpoints) generateCoverpointCode(cpp);
@ -504,6 +518,13 @@ class FunctionalCoverageVisitor final : public VNVisitor {
// Create implicit automatic bins if no regular bins exist
createImplicitAutoBins(coverpointp, exprp, autoBinMax);
// Eligible coverpoints route through the VlCoverpoint runtime; the rest (cross-fed or
// transition-bearing) keep the legacy per-bin-member path below.
if (coverpointConvertible(coverpointp)) {
generateConvertedCoverpoint(coverpointp, exprp, atLeastValue);
return;
}
// Generate member variables and matching code for each bin
// Process in two passes: first non-default bins, then default bins
std::vector<AstCoverBin*> defaultBins;
@ -595,47 +616,179 @@ class FunctionalCoverageVisitor final : public VNVisitor {
UINFO(4, " Successfully added if statement for bin: " << binp->name());
}
// Build the condition under which a default bin matches: NOT(OR of all normal bins).
AstNodeExpr* buildDefaultCondition(AstCoverpoint* coverpointp, AstNodeExpr* exprp,
FileLine* fl) {
AstNodeExpr* anyBinMatchp = nullptr;
for (AstNode* binp = coverpointp->binsp(); binp; binp = binp->nextp()) {
AstCoverBin* const cbinp = VN_AS(binp, CoverBin);
if (cbinp->binsType() == VCoverBinsType::BINS_DEFAULT
|| cbinp->binsType() == VCoverBinsType::BINS_IGNORE
|| cbinp->binsType() == VCoverBinsType::BINS_ILLEGAL)
continue;
AstNodeExpr* const binCondp = buildBinCondition(cbinp, exprp);
UASSERT_OBJ(binCondp, cbinp,
"buildBinCondition returned nullptr for non-ignore/non-illegal bin");
anyBinMatchp = anyBinMatchp ? new AstOr{fl, anyBinMatchp, binCondp} : binCondp;
}
return anyBinMatchp ? static_cast<AstNodeExpr*>(new AstNot{fl, anyBinMatchp})
: static_cast<AstNodeExpr*>(new AstConst{fl, AstConst::BitTrue{}});
}
//====================================================================
// VlCoverpoint conversion (eligible coverpoints)
// True if a coverpoint routes through the VlCoverpoint runtime. Cross-fed coverpoints
// (the cross reads their per-bin members) and transition-bearing ones stay legacy.
bool coverpointConvertible(AstCoverpoint* coverpointp) {
if (m_crossedCpNames.count(coverpointp->name())) return false;
for (AstNode* binp = coverpointp->binsp(); binp; binp = binp->nextp()) {
if (VN_AS(binp, CoverBin)->transp()) return false;
}
return true;
}
// 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};
refp->selfPointer(VSelfPointerText{VSelfPointerText::This{}});
return refp;
}
// Individual equality targets of an array bin (bins b[N] = {values/ranges}), in order.
std::vector<AstNodeExpr*> extractArrayValues(AstCoverBin* arrayBinp, AstNodeExpr* exprp) {
std::vector<AstNodeExpr*> values;
for (AstNode* rangep = arrayBinp->rangesp(); rangep; rangep = rangep->nextp()) {
if (AstInsideRange* const irp = VN_CAST(rangep, InsideRange)) {
AstConst* const minp = VN_CAST(V3Const::constifyEdit(irp->lhsp()), Const);
AstConst* const maxp = VN_CAST(V3Const::constifyEdit(irp->rhsp()), Const);
if (!minp || !maxp) {
arrayBinp->v3error("Non-constant expression in array bins range; "
"range bounds must be constants");
return values;
}
for (int val = minp->toSInt(); val <= maxp->toSInt(); ++val)
values.push_back(new AstConst{irp->fileline(), AstConst::WidthedValue{},
static_cast<int>(exprp->width()),
static_cast<uint32_t>(val)});
} else {
values.push_back(VN_AS(rangep->cloneTree(false), NodeExpr));
}
}
return values;
}
// Emit a 'this->m_cp.addSingleNamer/addArrayNamer(...)' statement for one bin
AstCStmt* makeNamer(AstVar* cpVarp, AstCoverBin* binp, int count) {
FileLine* const fl = binp->fileline();
AstCStmt* const cs = new AstCStmt{fl};
cs->add(memberRef(fl, cpVarp));
const std::string loc = "\"" + std::string{fl->filename()} + "\", "
+ std::to_string(fl->lineno()) + ", "
+ std::to_string(fl->firstColumn()) + ");";
if (count < 0) { // single bin
cs->add(".addSingleNamer(" + std::string{binp->binsType().binSetEnum()} + ", \""
+ binp->name() + "\", " + loc);
} else { // value array bin
cs->add(".addArrayNamer(" + std::string{binp->binsType().binSetEnum()} + ", "
+ std::to_string(count) + ", \"" + binp->name() + "\", " + loc);
}
return cs;
}
// Emit 'if (iff && cond) m_cp.incrementBin(idx);' (or recordHit, + illegal action) in sample()
void emitConvHitIf(AstCoverpoint* coverpointp, AstCoverBin* binp, AstVar* cpVarp, int idx,
AstNodeExpr* condp) {
FileLine* const fl = binp->fileline();
AstCStmt* const hitp = new AstCStmt{fl};
hitp->add(memberRef(fl, cpVarp));
hitp->add((binp->binsType().binIsNormal() ? ".incrementBin(" : ".recordHit(")
+ std::to_string(idx) + ");");
AstNode* actionp = hitp;
if (binp->binsType() == VCoverBinsType::BINS_ILLEGAL) {
actionp->addNext(makeIllegalBinAction(fl, "Illegal bin " + binp->prettyNameQ()
+ " hit in coverpoint "
+ coverpointp->prettyNameQ()));
}
AstNodeExpr* const guardedp = applyCoverpointIffCondition(coverpointp, fl, condp);
UASSERT_OBJ(m_sampleFuncp, binp, "sample() CFunc not set in converted coverpoint");
m_sampleFuncp->addStmtsp(new AstIf{fl, guardedp, actionp, nullptr});
}
// Route an eligible coverpoint through a VlCoverpoint member: emit the member, its
// sample() increments, the constructor configuration (init + namers), and registration.
void generateConvertedCoverpoint(AstCoverpoint* coverpointp, AstNodeExpr* exprp,
int atLeastValue) {
FileLine* const fl = coverpointp->fileline();
UINFO(4, " Converting coverpoint to VlCoverpoint: " << coverpointp->name());
if (!m_vlCoverpointDTypep) {
m_vlCoverpointDTypep = new AstCDType{fl, "VlCoverpoint"};
v3Global.rootp()->typeTablep()->addTypesp(m_vlCoverpointDTypep);
}
AstVar* const cpVarp = new AstVar{fl, VVarType::MEMBER, "__Vcp_" + coverpointp->name(),
m_vlCoverpointDTypep};
cpVarp->isStatic(false);
m_covergroupp->addMembersp(cpVarp);
m_convCpVars.push_back(cpVarp);
// Walk bins (non-default, then default), assigning sequential indices that match the
// namer append order; emit sample increments and collect namer statements.
std::vector<AstCStmt*> namerStmts;
std::vector<AstCoverBin*> defaultBins;
int idx = 0;
for (AstNode* binp = coverpointp->binsp(); binp; binp = binp->nextp()) {
AstCoverBin* const cbinp = VN_AS(binp, CoverBin);
if (cbinp->binsType() == VCoverBinsType::BINS_DEFAULT) {
defaultBins.push_back(cbinp);
continue;
}
if (cbinp->isArray()) { // value array: bins b[N] = {...} -> b[0]..b[N-1]
std::vector<AstNodeExpr*> values = extractArrayValues(cbinp, exprp);
namerStmts.push_back(makeNamer(cpVarp, cbinp, static_cast<int>(values.size())));
for (AstNodeExpr* valuep : values) {
emitConvHitIf(coverpointp, cbinp, cpVarp, idx++,
new AstEq{cbinp->fileline(), exprp->cloneTree(false), valuep});
}
} else {
namerStmts.push_back(makeNamer(cpVarp, cbinp, -1));
// buildBinCondition is null for 'ignore_bins = default' (no ranges); the bin
// still gets a reserved slot (recorded, never incremented).
if (AstNodeExpr* const condp = buildBinCondition(cbinp, exprp))
emitConvHitIf(coverpointp, cbinp, cpVarp, idx, condp);
++idx;
}
}
for (AstCoverBin* const defBinp : defaultBins) {
namerStmts.push_back(makeNamer(cpVarp, defBinp, -1));
emitConvHitIf(coverpointp, defBinp, cpVarp, idx++,
buildDefaultCondition(coverpointp, exprp, defBinp->fileline()));
}
// Constructor: init (allocates), namers, then registration (under --coverage)
const std::string hier = m_covergroupp->name() + "." + coverpointp->name();
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);
if (v3Global.opt.coverage()) {
AstCStmt* const regp = new AstCStmt{fl};
regp->add(memberRef(fl, cpVarp));
regp->add(".registerBins(vlSymsp->_vm_contextp__->coveragep(), \"v_covergroup/"
+ m_covergroupp->name() + "\");");
m_constructorp->addStmtsp(regp);
}
}
// Generate matching code for default bins
// Default bins match when value doesn't match any other explicit bin
void generateDefaultBinMatchCode(AstCoverpoint* coverpointp, AstCoverBin* defBinp,
AstNodeExpr* exprp, AstVar* hitVarp) {
UINFO(4, " Generating default bin match for: " << defBinp->name());
// Build OR of all non-default, non-ignore bins
AstNodeExpr* anyBinMatchp = nullptr;
for (AstNode* binp = coverpointp->binsp(); binp; binp = binp->nextp()) {
AstCoverBin* const cbinp = VN_AS(binp, CoverBin);
// Skip default, ignore, and illegal bins
if (cbinp->binsType() == VCoverBinsType::BINS_DEFAULT
|| cbinp->binsType() == VCoverBinsType::BINS_IGNORE
|| cbinp->binsType() == VCoverBinsType::BINS_ILLEGAL) {
continue;
}
// Build condition for this bin
AstNodeExpr* const binCondp = buildBinCondition(cbinp, exprp);
UASSERT_OBJ(binCondp, cbinp,
"buildBinCondition returned nullptr for non-ignore/non-illegal bin");
// OR with previous conditions
if (anyBinMatchp) {
anyBinMatchp = new AstOr{defBinp->fileline(), anyBinMatchp, binCondp};
} else {
anyBinMatchp = binCondp;
}
}
// Default matches when NO explicit bin matches
AstNodeExpr* defaultCondp = nullptr;
if (anyBinMatchp) {
// NOT (bin1 OR bin2 OR ... OR binN)
defaultCondp = new AstNot{defBinp->fileline(), anyBinMatchp};
} else {
// No other bins - default always matches (shouldn't happen in practice)
defaultCondp = new AstConst{defBinp->fileline(), AstConst::BitTrue{}};
}
AstNodeExpr* defaultCondp = buildDefaultCondition(coverpointp, exprp, defBinp->fileline());
// Apply iff condition if present
if (AstNodeExpr* iffp = coverpointp->iffp()) {
@ -1320,15 +1473,50 @@ class FunctionalCoverageVisitor final : public VNVisitor {
void generateCoverageMethodBody(AstFunc* funcp) {
FileLine* const fl = funcp->fileline();
AstVar* const returnVarp = VN_AS(funcp->fvarp(), Var);
// Count total bins (excluding ignore_bins and illegal_bins)
// Converted coverpoints hold their bins in VlCoverpoint. Combine their contributions
// (via coverageParts) with any remaining legacy cross/cross-fed bins as the same flat
// covered/total ratio the all-legacy path below computes. Normal bins only: ignore,
// illegal, and default are excluded (LRM 19.5).
if (!m_convCpVars.empty()) {
AstCStmt* const headp = new AstCStmt{fl};
headp->add("double __Vcov = 0.0; double __Vtot = 0.0;");
funcp->addStmtsp(headp);
for (AstVar* const cpVarp : m_convCpVars) {
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);
}
int legacyRegular = 0;
for (const BinInfo& bi : m_binInfos) {
if (!bi.binp->binsType().binIsNormal()) continue;
++legacyRegular;
AstCStmt* const cs = new AstCStmt{fl};
cs->add("if (");
cs->add(memberRef(fl, bi.varp));
cs->add(" >= " + std::to_string(bi.atLeast) + ") __Vcov += 1.0;");
funcp->addStmtsp(cs);
}
if (legacyRegular) {
AstCStmt* const cs = new AstCStmt{fl};
cs->add("__Vtot += " + std::to_string(legacyRegular) + ".0;");
funcp->addStmtsp(cs);
}
AstCStmt* const retp = new AstCStmt{fl};
retp->add(new AstVarRef{fl, returnVarp, VAccess::WRITE});
retp->add(" = (__Vtot != 0.0) ? (100.0 * __Vcov / __Vtot) : 100.0;");
funcp->addStmtsp(retp);
return;
}
// Count total bins (Normal only: excludes ignore/illegal/default)
int totalBins = 0;
for (const BinInfo& bi : m_binInfos) {
UINFO(6, " Bin: " << bi.binp->name() << " type=" << bi.binp->binsType().ascii());
if (bi.binp->binsType() != VCoverBinsType::BINS_IGNORE
&& bi.binp->binsType() != VCoverBinsType::BINS_ILLEGAL) {
totalBins++;
}
if (bi.binp->binsType().binIsNormal()) totalBins++;
}
UINFO(4, " Total regular bins: " << totalBins << " of " << m_binInfos.size());
@ -1337,7 +1525,6 @@ class FunctionalCoverageVisitor final : public VNVisitor {
// No coverage to compute - return 100%.
// Any parser-generated initialization of returnVar is overridden by our assignment.
UINFO(4, " Empty covergroup, returning 100.0");
AstVar* const returnVarp = VN_AS(funcp->fvarp(), Var);
funcp->addStmtsp(new AstAssign{fl, new AstVarRef{fl, returnVarp, VAccess::WRITE},
new AstConst{fl, AstConst::RealDouble{}, 100.0}});
UINFO(4, " Added assignment to return 100.0");
@ -1356,11 +1543,8 @@ class FunctionalCoverageVisitor final : public VNVisitor {
// For each regular bin, if count > 0, increment covered_count
for (const BinInfo& bi : m_binInfos) {
// Skip ignore_bins and illegal_bins in coverage calculation
if (bi.binp->binsType() == VCoverBinsType::BINS_IGNORE
|| bi.binp->binsType() == VCoverBinsType::BINS_ILLEGAL) {
continue;
}
// Skip ignore/illegal/default bins in coverage calculation
if (!bi.binp->binsType().binIsNormal()) continue;
// if (bin_count >= at_least) covered_count++;
AstIf* ifp = new AstIf{
@ -1375,9 +1559,6 @@ class FunctionalCoverageVisitor final : public VNVisitor {
funcp->addStmtsp(ifp);
}
// Find the return variable
AstVar* const returnVarp = VN_AS(funcp->fvarp(), Var);
// Calculate coverage: (covered_count / total_bins) * 100.0
// return_var = (double)covered_count / (double)total_bins * 100.0

View File

@ -689,6 +689,7 @@ class EmitCHeader final : public EmitCConstInit {
if (v3Global.opt.mtasks()) puts("#include \"verilated_threads.h\"\n");
if (v3Global.opt.savable()) puts("#include \"verilated_save.h\"\n");
if (v3Global.opt.coverage()) puts("#include \"verilated_cov.h\"\n");
if (v3Global.opt.coverage()) puts("#include \"verilated_covergroup.h\"\n");
if (v3Global.usesTiming()) puts("#include \"verilated_timing.h\"\n");
if (v3Global.useRandomizeMethods()) puts("#include \"verilated_random.h\"\n");
if (v3Global.usesForce()) puts("#include \"verilated_force.h\"\n");

View File

@ -70,6 +70,7 @@ class EmitCModel final : public EmitCFunc {
if (v3Global.opt.mtasks()) puts("#include \"verilated_threads.h\"\n");
if (v3Global.opt.savable()) puts("#include \"verilated_save.h\"\n");
if (v3Global.opt.coverage()) puts("#include \"verilated_cov.h\"\n");
if (v3Global.opt.coverage()) puts("#include \"verilated_covergroup.h\"\n");
if (v3Global.dpi()) puts("#include \"svdpi.h\"\n");
// Declare foreign instances up front to make C++ happy

View File

@ -245,6 +245,7 @@ std::vector<std::string> V3Global::verilatedCppFiles() {
if (v3Global.opt.vpi()) result.emplace_back("verilated_vpi.cpp");
if (v3Global.opt.savable()) result.emplace_back("verilated_save.cpp");
if (v3Global.opt.coverage()) result.emplace_back("verilated_cov.cpp");
if (v3Global.opt.coverage()) result.emplace_back("verilated_covergroup.cpp");
for (const string& base : v3Global.opt.traceSourceBases())
result.emplace_back(base + "_c.cpp");
if (v3Global.usesProbDist()) result.emplace_back("verilated_probdist.cpp");

View File

@ -1,4 +1,12 @@
cg.data.grouped: 2
cg.data.values: 3
cg2.cp.range_arr: 3
cg3.cp.range_sized: 3
cg.data.values[0]: 1
cg.data.values[1]: 1
cg.data.values[2]: 1
cg2.cp.range_arr[0]: 1
cg2.cp.range_arr[1]: 1
cg2.cp.range_arr[2]: 1
cg2.cp.range_arr[3]: 0
cg3.cp.range_sized[0]: 1
cg3.cp.range_sized[1]: 1
cg3.cp.range_sized[2]: 1
cg3.cp.range_sized[3]: 0

View File

@ -65,6 +65,15 @@ cg_at_least.cp_addr.addr0: 1
cg_at_least.cp_addr.addr1: 1
cg_at_least.cp_cmd.read: 1
cg_at_least.cp_cmd.write: 1
cg_def_cross.axc.a0_x_read [cross]: 1
cg_def_cross.axc.a0_x_write [cross]: 0
cg_def_cross.axc.a1_x_read [cross]: 0
cg_def_cross.axc.a1_x_write [cross]: 0
cg_def_cross.cp_a.a0: 1
cg_def_cross.cp_a.a1: 0
cg_def_cross.cp_a.ad: 1
cg_def_cross.cp_c.read: 1
cg_def_cross.cp_c.write: 1
cg_goal.addr_cmd_goal.addr0_x_read [cross]: 1
cg_goal.addr_cmd_goal.addr0_x_write [cross]: 0
cg_goal.addr_cmd_goal.addr1_x_read [cross]: 0
@ -82,6 +91,16 @@ cg_ignore.cross_ab.a0_x_read [cross]: 1
cg_ignore.cross_ab.a0_x_write [cross]: 1
cg_ignore.cross_ab.a1_x_read [cross]: 1
cg_ignore.cross_ab.a1_x_write [cross]: 1
cg_mixed.ab.addr0_x_read [cross]: 1
cg_mixed.ab.addr0_x_write [cross]: 1
cg_mixed.ab.addr1_x_read [cross]: 1
cg_mixed.ab.addr1_x_write [cross]: 1
cg_mixed.cp_addr.addr0: 2
cg_mixed.cp_addr.addr1: 2
cg_mixed.cp_cmd.read: 2
cg_mixed.cp_cmd.write: 2
cg_mixed.cp_solo.debug: 2
cg_mixed.cp_solo.normal: 2
cg_range.addr_cmd_range.hi_range_x_read [cross]: 1
cg_range.addr_cmd_range.hi_range_x_write [cross]: 1
cg_range.addr_cmd_range.lo_range_x_read [cross]: 1

View File

@ -99,6 +99,23 @@ module t;
cross cp_a, cp_c; // no label: reported under the default cross name
endgroup
// Cross plus an un-crossed coverpoint: get_inst_coverage must combine the converted
// (VlCoverpoint) coverpoint cp_solo with the legacy cross/crossed-coverpoint bins.
covergroup cg_mixed;
cp_addr: coverpoint addr {bins addr0 = {0}; bins addr1 = {1};}
cp_cmd: coverpoint cmd {bins read = {0}; bins write = {1};}
cp_solo: coverpoint mode {bins normal = {0}; bins debug = {1};} // not crossed
ab: cross cp_addr, cp_cmd;
endgroup
// Crossed (hence non-convertible) coverpoint that also has a default bin: exercises the
// legacy default-bin codegen path that converted coverpoints bypass.
covergroup cg_def_cross;
cp_a: coverpoint addr iff (mode) {bins a0 = {0}; bins a1 = {1}; bins ad = default;}
cp_c: coverpoint cmd {bins read = {0}; bins write = {1};}
axc: cross cp_a, cp_c;
endgroup
cg2 cg2_inst = new;
cg_ignore cg_ignore_inst = new;
cg_range cg_range_inst = new;
@ -109,6 +126,8 @@ module t;
cg_goal cg_goal_inst = new;
cg_unsup_cross_opt cg_unsup_cross_opt_inst = new;
cg_unnamed_cross cg_unnamed_cross_inst = new;
cg_mixed cg_mixed_inst = new;
cg_def_cross cg_def_cross_inst = new;
initial begin
// Sample 2-way: hit all 4 combinations
@ -275,6 +294,23 @@ module t;
cg_unnamed_cross_inst.sample(); // a1 x write
`checkr(cg_unnamed_cross_inst.get_inst_coverage(), 75.0);
// Sample cg_mixed: 10 bins total (cp_addr 2 + cp_cmd 2 + cp_solo 2 + cross ab 4)
addr = 0; cmd = 0; mode = 0;
cg_mixed_inst.sample(); // addr0, read, solo normal, ab(addr0_x_read)
`checkr(cg_mixed_inst.get_inst_coverage(), 40.0); // 4/10
addr = 0; cmd = 1; mode = 1;
cg_mixed_inst.sample(); // addr0, write, solo debug, ab(addr0_x_write)
addr = 1; cmd = 0; mode = 0;
cg_mixed_inst.sample(); // addr1, read, ab(addr1_x_read)
addr = 1; cmd = 1; mode = 1;
cg_mixed_inst.sample(); // addr1, write, ab(addr1_x_write)
`checkr(cg_mixed_inst.get_inst_coverage(), 100.0); // 10/10
// Sample cg_def_cross (default bin in a crossed coverpoint, gated by iff)
mode = 1;
addr = 0; cmd = 0; cg_def_cross_inst.sample(); // a0, read
addr = 2; cmd = 1; cg_def_cross_inst.sample(); // ad (default), write
$write("*-* All Finished *-*\n");
$finish;
end

View File

@ -1,11 +1,11 @@
cg.data.high: 1
cg.data.low: 1
cg.data.other: 2
cg2.cp_only_default.all: 4
cg.data.other [default]: 2
cg2.cp_only_default.all [default]: 4
cg3.data.bad [ignore]: 1
cg3.data.err [illegal]: 0
cg3.data.normal: 2
cg3.data.other: 2
cg3.data.other [default]: 2
cg4.cp_idx.auto_0: 1
cg4.cp_idx.auto_1: 1
cg4.cp_idx.auto_2: 1

View File

@ -157,17 +157,17 @@ module t;
// Sample cg3: verify ignore/illegal bins do not contribute to coverage
data = 2;
cg3_inst.sample(); // hits normal bin
`checkr(cg3_inst.get_inst_coverage(), 50.0); // 1/2 bins hit (normal)
`checkr(cg3_inst.get_inst_coverage(), 100.0); // 1/1: only 'normal' counts (default/ignore/illegal excluded, LRM 19.5)
data = 7;
cg3_inst.sample(); // hits normal bin again
`checkr(cg3_inst.get_inst_coverage(), 50.0); // no new bins
`checkr(cg3_inst.get_inst_coverage(), 100.0); // no new normal bins
data = 255;
cg3_inst.sample(); // ignore_bins value; Verilator counts it toward default bin
`checkr(cg3_inst.get_inst_coverage(), 100.0); // 2/2: Verilator hits 'other' (default) even for ignore_bins
cg3_inst.sample(); // ignore_bins value; recorded but excluded from coverage
`checkr(cg3_inst.get_inst_coverage(), 100.0);
// note: do not sample 254 (illegal_bins would cause runtime assertion)
data = 100;
cg3_inst.sample(); // hits default (other) bin
`checkr(cg3_inst.get_inst_coverage(), 100.0); // 2/2 bins hit
cg3_inst.sample(); // hits default (other) bin; recorded but excluded from coverage
`checkr(cg3_inst.get_inst_coverage(), 100.0);
// Sample cg4: auto-bins with one excluded value
// idx=2 is in ignore_bins, so auto-bins cover 0, 1, 3 only (3 bins total)

View File

@ -2,9 +2,11 @@ cg_and.cp_concat.b00: 0
cg_and.cp_concat.b01: 1
cg_and.cp_concat.b10: 0
cg_and.cp_concat.b11: 0
cg_array_iff.cp.arr: 1
cg_array_iff.cp.arr[0]: 1
cg_array_iff.cp.arr[1]: 0
cg_array_iff.cp.arr[2]: 0
cg_bitw.cp.seven: 1
cg_default_iff.cp.def: 1
cg_default_iff.cp.def [default]: 1
cg_default_iff.cp.known: 1
cg_iff.cp_value.disabled_hi: 0
cg_iff.cp_value.disabled_lo: 0

View File

@ -129,7 +129,7 @@ module t;
enable = 1;
value = 10;
cg2.sample(); // hits 'known'
`checkr(cg2.get_inst_coverage(), 50.0);
`checkr(cg2.get_inst_coverage(), 100.0); // 1/1: only 'known' counts (default excluded, LRM 19.5)
value = 99;
cg2.sample(); // hits 'def' (default)
`checkr(cg2.get_inst_coverage(), 100.0);

View File

@ -1,5 +1,7 @@
cg.data.arr [ignore]: 0
cg.data.bad [illegal]: 0
cg.data.arr[0] [ignore]: 0
cg.data.arr[1] [ignore]: 0
cg.data.bad[0] [illegal]: 0
cg.data.bad[1] [illegal]: 0
cg.data.catch_all [ignore]: 0
cg.data.high: 1
cg.data.low: 1

View File

@ -2,7 +2,9 @@ cg.data.forbidden [illegal]: 0
cg.data.high: 1
cg.data.low: 1
cg.data.mid: 1
cg2.cp_arr.bad_arr [illegal]: 0
cg2.cp_arr.bad_arr[0] [illegal]: 0
cg2.cp_arr.bad_arr[1] [illegal]: 0
cg2.cp_arr.bad_arr[2] [illegal]: 0
cg2.cp_arr.ok: 1
cg2.cp_arr.wlib [illegal]: 0
cg2.cp_trans.bad_2step [illegal]: 0

View File

@ -7,3 +7,5 @@ cg.cp_trans2.trans2: 1
cg.cp_trans2.trans3: 1
cg.cp_trans3.seq_a: 1
cg.cp_trans3.seq_b: 1
cg_legacy.cp_mix.tr: 1
cg_legacy.cp_mix.vals: 1

View File

@ -39,7 +39,17 @@ module t;
}
endgroup
// Non-convertible coverpoint (it has a transition bin) that also carries a value-array bin,
// so the legacy array codegen path (which converted coverpoints bypass) is still exercised.
covergroup cg_legacy;
cp_mix: coverpoint state {
bins tr = (0 => 1);
bins vals[] = {2, [4:5]}; // discrete + range elements (both legacy array paths)
}
endgroup
cg cg_inst = new;
cg_legacy cg_legacy_inst = new;
initial begin
// Drive sequence 0->1->2->3->4 which hits all bins
@ -56,6 +66,12 @@ module t;
cg_inst.sample(); // 3=>4: seq_b done
`checkr(cg_inst.get_inst_coverage(), 100.0);
// cg_legacy: exercise legacy array codegen (non-convertible coverpoint)
state = 0; cg_legacy_inst.sample();
state = 1; cg_legacy_inst.sample(); // 0=>1: tr
state = 2; cg_legacy_inst.sample(); // vals
state = 3; cg_legacy_inst.sample(); // vals
$write("*-* All Finished *-*\n");
$finish;
end

View File

@ -14,9 +14,9 @@
module t;
%000001 logic [1:0] addr;
-000001 point: type=toggle comment=addr[0]:0->1 hier=top.t
-000000 point: type=toggle comment=addr[0]:0->1 hier=top.t
-000000 point: type=toggle comment=addr[0]:1->0 hier=top.t
-000000 point: type=toggle comment=addr[1]:0->1 hier=top.t
-000001 point: type=toggle comment=addr[1]:0->1 hier=top.t
-000000 point: type=toggle comment=addr[1]:1->0 hier=top.t
%000001 logic cmd;
-000001 point: type=toggle comment=cmd:0->1 hier=top.t
@ -278,6 +278,50 @@
// cross: [a1, write]
endgroup
// Cross plus an un-crossed coverpoint: get_inst_coverage must combine the converted
// (VlCoverpoint) coverpoint cp_solo with the legacy cross/crossed-coverpoint bins.
covergroup cg_mixed;
%000002 cp_addr: coverpoint addr {bins addr0 = {0}; bins addr1 = {1};}
-000002 point: type=covergroup comment= hier=cg_mixed.cp_addr.addr0
-000002 point: type=covergroup comment= hier=cg_mixed.cp_addr.addr1
%000002 cp_cmd: coverpoint cmd {bins read = {0}; bins write = {1};}
-000002 point: type=covergroup comment= hier=cg_mixed.cp_cmd.read
-000002 point: type=covergroup comment= hier=cg_mixed.cp_cmd.write
%000002 cp_solo: coverpoint mode {bins normal = {0}; bins debug = {1};} // not crossed
-000002 point: type=covergroup comment= hier=cg_mixed.cp_solo.normal
-000002 point: type=covergroup comment= hier=cg_mixed.cp_solo.debug
%000001 ab: cross cp_addr, cp_cmd;
-000001 point: type=covergroup comment= hier=cg_mixed.ab.addr0_x_read
// cross: [addr0, read]
-000001 point: type=covergroup comment= hier=cg_mixed.ab.addr0_x_write
// cross: [addr0, write]
-000001 point: type=covergroup comment= hier=cg_mixed.ab.addr1_x_read
// cross: [addr1, read]
-000001 point: type=covergroup comment= hier=cg_mixed.ab.addr1_x_write
// cross: [addr1, write]
endgroup
// Crossed (hence non-convertible) coverpoint that also has a default bin: exercises the
// legacy default-bin codegen path that converted coverpoints bypass.
covergroup cg_def_cross;
%000001 cp_a: coverpoint addr iff (mode) {bins a0 = {0}; bins a1 = {1}; bins ad = default;}
-000001 point: type=covergroup comment= hier=cg_def_cross.cp_a.a0
-000000 point: type=covergroup comment= hier=cg_def_cross.cp_a.a1
-000001 point: type=covergroup comment= hier=cg_def_cross.cp_a.ad
%000001 cp_c: coverpoint cmd {bins read = {0}; bins write = {1};}
-000001 point: type=covergroup comment= hier=cg_def_cross.cp_c.read
-000001 point: type=covergroup comment= hier=cg_def_cross.cp_c.write
%000001 axc: cross cp_a, cp_c;
-000001 point: type=covergroup comment= hier=cg_def_cross.axc.a0_x_read
// cross: [a0, read]
-000000 point: type=covergroup comment= hier=cg_def_cross.axc.a0_x_write
// cross: [a0, write]
-000000 point: type=covergroup comment= hier=cg_def_cross.axc.a1_x_read
// cross: [a1, read]
-000000 point: type=covergroup comment= hier=cg_def_cross.axc.a1_x_write
// cross: [a1, write]
endgroup
%000001 cg2 cg2_inst = new;
-000001 point: type=line comment=block hier=top.t
%000001 cg_ignore cg_ignore_inst = new;
@ -298,6 +342,10 @@
-000001 point: type=line comment=block hier=top.t
%000001 cg_unnamed_cross cg_unnamed_cross_inst = new;
-000001 point: type=line comment=block hier=top.t
%000001 cg_mixed cg_mixed_inst = new;
-000001 point: type=line comment=block hier=top.t
%000001 cg_def_cross cg_def_cross_inst = new;
-000001 point: type=line comment=block hier=top.t
%000001 initial begin
-000001 point: type=line comment=block hier=top.t
@ -641,6 +689,40 @@
-000000 point: type=line comment=block hier=top.t
-000001 point: type=line comment=else hier=top.t
// Sample cg_mixed: 10 bins total (cp_addr 2 + cp_cmd 2 + cp_solo 2 + cross ab 4)
%000001 addr = 0; cmd = 0; mode = 0;
-000001 point: type=line comment=block hier=top.t
%000001 cg_mixed_inst.sample(); // addr0, read, solo normal, ab(addr0_x_read)
-000001 point: type=line comment=block hier=top.t
%000001 `checkr(cg_mixed_inst.get_inst_coverage(), 40.0); // 4/10
-000001 point: type=line comment=block hier=top.t
-000000 point: type=line comment=block hier=top.t
-000001 point: type=line comment=else hier=top.t
%000001 addr = 0; cmd = 1; mode = 1;
-000001 point: type=line comment=block hier=top.t
%000001 cg_mixed_inst.sample(); // addr0, write, solo debug, ab(addr0_x_write)
-000001 point: type=line comment=block hier=top.t
%000001 addr = 1; cmd = 0; mode = 0;
-000001 point: type=line comment=block hier=top.t
%000001 cg_mixed_inst.sample(); // addr1, read, ab(addr1_x_read)
-000001 point: type=line comment=block hier=top.t
%000001 addr = 1; cmd = 1; mode = 1;
-000001 point: type=line comment=block hier=top.t
%000001 cg_mixed_inst.sample(); // addr1, write, ab(addr1_x_write)
-000001 point: type=line comment=block hier=top.t
%000001 `checkr(cg_mixed_inst.get_inst_coverage(), 100.0); // 10/10
-000001 point: type=line comment=block hier=top.t
-000000 point: type=line comment=block hier=top.t
-000001 point: type=line comment=else hier=top.t
// Sample cg_def_cross (default bin in a crossed coverpoint, gated by iff)
%000001 mode = 1;
-000001 point: type=line comment=block hier=top.t
%000001 addr = 0; cmd = 0; cg_def_cross_inst.sample(); // a0, read
-000001 point: type=line comment=block hier=top.t
%000001 addr = 2; cmd = 1; cg_def_cross_inst.sample(); // ad (default), write
-000001 point: type=line comment=block hier=top.t
%000001 $write("*-* All Finished *-*\n");
-000001 point: type=line comment=block hier=top.t
%000001 $finish;