This commit is contained in:
Marco Bartoli 2026-07-04 00:00:32 -07:00 committed by GitHub
commit a044cd1dbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 948 additions and 129 deletions

View File

@ -2103,15 +2103,16 @@ private:
// MEMBERS // MEMBERS
T_Class* m_objp = nullptr; // Object pointed to T_Class* m_objp = nullptr; // Object pointed to
bool m_weak = false; // Non-owning reference
// METHODS // METHODS
// Increase reference counter with null check // Increase reference counter with null check
void refCountInc() const VL_MT_SAFE { void refCountInc() const VL_MT_SAFE {
if (m_objp) m_objp->refCountInc(); if (m_objp && !m_weak) m_objp->refCountInc();
} }
// Decrease reference counter with null check // Decrease reference counter with null check
void refCountDec() const VL_MT_SAFE { void refCountDec() const VL_MT_SAFE {
if (m_objp) m_objp->refCountDec(); if (m_objp && !m_weak) m_objp->refCountDec();
} }
public: public:
@ -2119,7 +2120,7 @@ public:
VlClassRef() = default; VlClassRef() = default;
// Init with nullptr // Init with nullptr
// cppcheck-suppress noExplicitConstructor // cppcheck-suppress noExplicitConstructor
VlClassRef(VlNull){}; VlClassRef(VlNull) {};
template <typename... T_Args> template <typename... T_Args>
VlClassRef(VlDeleter& deleter, T_Args&&... args) VlClassRef(VlDeleter& deleter, T_Args&&... args)
: m_objp{new T_Class} { : m_objp{new T_Class} {
@ -2151,58 +2152,79 @@ public:
} }
// cppcheck-suppress noExplicitConstructor // cppcheck-suppress noExplicitConstructor
VlClassRef(const VlClassRef& copied) VlClassRef(const VlClassRef& copied)
: m_objp{copied.m_objp} { : m_objp{copied.m_objp}
, m_weak{copied.m_weak} {
refCountInc(); refCountInc();
} }
// cppcheck-suppress noExplicitConstructor // cppcheck-suppress noExplicitConstructor
VlClassRef(VlClassRef&& moved) VlClassRef(VlClassRef&& moved)
: m_objp{std::exchange(moved.m_objp, nullptr)} {} : m_objp{std::exchange(moved.m_objp, nullptr)}
, m_weak{std::exchange(moved.m_weak, false)} {}
// cppcheck-suppress noExplicitConstructor // cppcheck-suppress noExplicitConstructor
template <typename T_OtherClass> template <typename T_OtherClass>
VlClassRef(const VlClassRef<T_OtherClass>& copied) VlClassRef(const VlClassRef<T_OtherClass>& copied)
: m_objp{copied.m_objp} { : m_objp{copied.m_objp}
, m_weak{copied.m_weak} {
refCountInc(); refCountInc();
} }
// cppcheck-suppress noExplicitConstructor // cppcheck-suppress noExplicitConstructor
template <typename T_OtherClass> template <typename T_OtherClass>
VlClassRef(VlClassRef<T_OtherClass>&& moved) VlClassRef(VlClassRef<T_OtherClass>&& moved)
: m_objp{std::exchange(moved.m_objp, nullptr)} {} : m_objp{std::exchange(moved.m_objp, nullptr)}
, m_weak{std::exchange(moved.m_weak, false)} {}
~VlClassRef() { refCountDec(); } ~VlClassRef() { refCountDec(); }
// METHODS // METHODS
/// Create a non-owning ("weak") reference: it does not change the reference count, so the
/// pointed-to object must be kept alive by some other (strong) reference for as long as
/// this one is used. Dereferencing a weak reference after the object has been freed is
/// undefined. Used only for an embedded covergroup's back-pointer to its enclosing
/// object, which the enclosing object transitively owns, so the back-pointer is always
/// outlived by a strong reference (IEEE 1800-2023 19.4).
static VlClassRef weak(T_Class* objp) {
VlClassRef ref;
ref.m_objp = objp;
ref.m_weak = true;
return ref;
}
// Copy and move assignments // Copy and move assignments
VlClassRef& operator=(const VlClassRef& copied) { VlClassRef& operator=(const VlClassRef& copied) {
if (m_objp == copied.m_objp) return *this; if (m_objp == copied.m_objp && m_weak == copied.m_weak) return *this;
refCountDec(); refCountDec();
m_objp = copied.m_objp; m_objp = copied.m_objp;
m_weak = copied.m_weak;
refCountInc(); refCountInc();
return *this; return *this;
} }
VlClassRef& operator=(VlClassRef&& moved) { VlClassRef& operator=(VlClassRef&& moved) {
if (m_objp == moved.m_objp) return *this; if (m_objp == moved.m_objp && m_weak == moved.m_weak) return *this;
refCountDec(); refCountDec();
m_objp = std::exchange(moved.m_objp, nullptr); m_objp = std::exchange(moved.m_objp, nullptr);
m_weak = std::exchange(moved.m_weak, false);
return *this; return *this;
} }
template <typename T_OtherClass> template <typename T_OtherClass>
VlClassRef& operator=(const VlClassRef<T_OtherClass>& copied) { VlClassRef& operator=(const VlClassRef<T_OtherClass>& copied) {
if (m_objp == copied.m_objp) return *this; if (m_objp == copied.m_objp && m_weak == copied.m_weak) return *this;
refCountDec(); refCountDec();
m_objp = copied.m_objp; m_objp = copied.m_objp;
m_weak = copied.m_weak;
refCountInc(); refCountInc();
return *this; return *this;
} }
template <typename T_OtherClass> template <typename T_OtherClass>
VlClassRef& operator=(VlClassRef<T_OtherClass>&& moved) { VlClassRef& operator=(VlClassRef<T_OtherClass>&& moved) {
if (m_objp == moved.m_objp) return *this; if (m_objp == moved.m_objp && m_weak == moved.m_weak) return *this;
refCountDec(); refCountDec();
m_objp = std::exchange(moved.m_objp, nullptr); m_objp = std::exchange(moved.m_objp, nullptr);
m_weak = std::exchange(moved.m_weak, false);
return *this; return *this;
} }
// Assign with nullptr // Assign with nullptr
VlClassRef& operator=(VlNull) { VlClassRef& operator=(VlNull) {
refCountDec(); refCountDec();
m_objp = nullptr; m_objp = nullptr;
m_weak = false;
return *this; return *this;
} }
// Dynamic caster // Dynamic caster
@ -2253,6 +2275,14 @@ static inline bool VL_CAST_DYNAMIC(VlClassRef<T_Lhs> in, VlClassRef<T_Out>& outr
return false; return false;
} }
// Create a non-owning ("weak") reference to an existing object, deducing the class type from
// the pointer. See VlClassRef::weak; used for an embedded covergroup's back-pointer to its
// enclosing object so the back-pointer does not keep the enclosing object alive.
template <typename T_Class>
static inline VlClassRef<T_Class> VL_WEAK_REF(T_Class* objp) {
return VlClassRef<T_Class>::weak(objp);
}
template <typename T_Lhs> template <typename T_Lhs>
static inline bool VL_CAST_DYNAMIC(VlNull, VlClassRef<T_Lhs>& outr) { static inline bool VL_CAST_DYNAMIC(VlNull, VlClassRef<T_Lhs>& outr) {
outr = VlNull{}; outr = VlNull{};

View File

@ -30,6 +30,7 @@
#include "V3MemberMap.h" #include "V3MemberMap.h"
#include <set> #include <set>
#include <vector>
VL_DEFINE_DEBUG_FUNCTIONS; VL_DEFINE_DEBUG_FUNCTIONS;
@ -44,6 +45,8 @@ class FunctionalCoverageVisitor final : public VNVisitor {
// STATE // STATE
AstClass* m_covergroupp = nullptr; // Current covergroup being processed AstClass* m_covergroupp = nullptr; // Current covergroup being processed
AstClass* m_enclosingClassp = nullptr; // Class lexically enclosing the covergroup (if any)
AstVar* m_embeddedVarp = nullptr; // Enclosing class member that owns the embedded covergroup
AstFunc* m_sampleFuncp = nullptr; // Current sample() function AstFunc* m_sampleFuncp = nullptr; // Current sample() function
AstFunc* m_constructorp = nullptr; // Current constructor AstFunc* m_constructorp = nullptr; // Current constructor
std::vector<AstCoverpoint*> m_coverpoints; // Coverpoints in current covergroup std::vector<AstCoverpoint*> m_coverpoints; // Coverpoints in current covergroup
@ -68,6 +71,22 @@ class FunctionalCoverageVisitor final : public VNVisitor {
, crossBins{cb} {} , crossBins{cb} {}
}; };
std::vector<BinInfo> m_binInfos; // All bins in current covergroup std::vector<BinInfo> m_binInfos; // All bins in current covergroup
struct EmbeddedEventTrigger final {
FileLine* eventFl; // Source location of the clocking-event item (for diagnostics)
AstVar* baseVarp; // Enclosing-class member naming the event signal
AstVar* memberVarp; // Sub-member when the event is 'base.member', else nullptr
VEdgeType edgeType; // Edge sensitivity (posedge/negedge/changed)
AstVar* prevVarp; // Previous-value member; created lazily during install
EmbeddedEventTrigger(FileLine* eventFl, AstVar* baseVarp, AstVar* memberVarp,
VEdgeType edgeType, AstVar* prevVarp)
: eventFl{eventFl}
, baseVarp{baseVarp}
, memberVarp{memberVarp}
, edgeType{edgeType}
, prevVarp{prevVarp} {}
};
std::set<std::string> m_crossedCpNames; // Coverpoints referenced by a cross (kept legacy) std::set<std::string> m_crossedCpNames; // Coverpoints referenced by a cross (kept legacy)
std::vector<AstVar*> m_convCpVars; // VlCoverpoint members of converted coverpoints std::vector<AstVar*> m_convCpVars; // VlCoverpoint members of converted coverpoints
AstCDType* m_vlCoverpointDTypep = nullptr; // Shared "VlCoverpoint" C++ member type AstCDType* m_vlCoverpointDTypep = nullptr; // Shared "VlCoverpoint" C++ member type
@ -1692,9 +1711,246 @@ class FunctionalCoverageVisitor final : public VNVisitor {
} }
// VISITORS // VISITORS
AstNode* findEnclosingMemberRef(AstClass* cgClassp) { void collectOwnVars(std::set<const AstVar*>& ownVars) {
for (AstNode* itemp = m_covergroupp->membersp(); itemp; itemp = itemp->nextp()) {
if (const AstVar* const varp = VN_CAST(itemp, Var)) ownVars.insert(varp);
}
}
void collectEnclosingVars(std::set<const AstVar*>& enclosingVars) {
if (!m_enclosingClassp) return;
m_enclosingClassp->foreachMember(
[&](AstClass* const, AstVar* const varp) { enclosingVars.insert(varp); });
}
bool isEmbeddedCovergroupVar(const AstVar* varp) const {
if (!varp || !varp->isClassMember()) return false;
const AstClassRefDType* const refp = VN_CAST(varp->dtypep()->skipRefp(), ClassRefDType);
return refp && refp->classp() == m_covergroupp;
}
AstVar* findEmbeddedCovergroupVar() const {
// Return the enclosing-class member that holds this embedded covergroup instance.
// An embedded covergroup declaration declares a single anonymous type and a single
// instance variable of that type (IEEE 1800-2023 19.4); a second variable of the same
// anonymous type is not expressible, so the first match is the sole instance.
if (!m_enclosingClassp) return nullptr;
for (AstNode* itemp = m_enclosingClassp->membersp(); itemp; itemp = itemp->nextp()) {
if (AstVar* const varp = VN_CAST(itemp, Var)) {
if (isEmbeddedCovergroupVar(varp)) return varp;
}
}
return nullptr;
}
AstNodeExpr* newEnclosingHandleRef(FileLine* fl, AstVar* handleVarp) {
return new AstVarRef{fl, handleVarp, VAccess::READ};
}
bool rewriteThisRef(AstThisRef* refp, AstVar* handleVarp) {
if (!m_enclosingClassp) return false;
const AstClassRefDType* const refDTypep
= VN_CAST(refp->dtypep()->skipRefp(), ClassRefDType);
if (!refDTypep || refDTypep->classp() != m_covergroupp) return false;
AstNodeExpr* const newp = newEnclosingHandleRef(refp->fileline(), handleVarp);
refp->replaceWith(newp);
VL_DO_DANGLING(pushDeletep(refp), refp);
return true;
}
void rewriteVarRef(AstVarRef* refp, AstVar* handleVarp) {
FileLine* const fl = refp->fileline();
AstMemberSel* const selp
= new AstMemberSel{fl, newEnclosingHandleRef(fl, handleVarp), refp->varp()};
selp->access(refp->access());
refp->replaceWith(selp);
VL_DO_DANGLING(pushDeletep(refp), refp);
}
bool parseEmbeddedEventExpr(AstNodeExpr* exprp, AstVar*& baseVarp, AstVar*& memberVarp) const {
if (AstVarRef* const refp = VN_CAST(exprp, VarRef)) {
if (!refp->varp()->isClassMember()) return false;
baseVarp = refp->varp();
memberVarp = nullptr;
return true;
}
AstMemberSel* const selp = VN_CAST(exprp, MemberSel);
if (!selp) return false;
AstVarRef* const baseRefp = VN_CAST(selp->fromp(), VarRef);
if (!baseRefp || !baseRefp->varp()->isClassMember()) return false;
baseVarp = baseRefp->varp();
memberVarp = selp->varp();
return true;
}
bool isEventLvalue(AstNodeExpr* exprp, const EmbeddedEventTrigger& trigger) const {
AstVar* baseVarp = nullptr;
AstVar* memberVarp = nullptr;
if (!parseEmbeddedEventExpr(exprp, baseVarp, memberVarp)) return false;
return baseVarp == trigger.baseVarp && memberVarp == trigger.memberVarp;
}
AstNodeExpr* newEventRead(FileLine* fl, const EmbeddedEventTrigger& trigger) {
AstNodeExpr* const basep = new AstVarRef{fl, trigger.baseVarp, VAccess::READ};
if (!trigger.memberVarp) return basep;
AstMemberSel* const selp = new AstMemberSel{fl, basep, trigger.memberVarp};
selp->access(VAccess::READ);
return selp;
}
AstNodeDType* eventDTypep(const EmbeddedEventTrigger& trigger) const {
if (trigger.memberVarp) return trigger.memberVarp->dtypep();
return trigger.baseVarp->dtypep();
}
string eventPrevName(const EmbeddedEventTrigger& trigger) const {
string name = "__Vcg_prev_" + m_embeddedVarp->name() + "_" + trigger.baseVarp->name();
if (trigger.memberVarp) name += "_" + trigger.memberVarp->name();
return name;
}
AstVar* newEventPrevVar(FileLine* fl, const EmbeddedEventTrigger& trigger) {
return new AstVar{fl, VVarType::MEMBER, eventPrevName(trigger), eventDTypep(trigger)};
}
bool isEmbeddedEventExpr(AstNodeExpr* exprp) const {
AstVar* baseVarp = nullptr;
AstVar* memberVarp = nullptr;
return parseEmbeddedEventExpr(exprp, baseVarp, memberVarp);
}
bool hasEmbeddedEventExpr(AstCovergroup* cgp) const {
if (!m_enclosingClassp || !m_embeddedVarp || !cgp || !cgp->eventp()) return false;
for (AstNode* senp = cgp->eventp()->sensesp(); senp; senp = senp->nextp()) {
AstSenItem* const itemp = VN_AS(senp, SenItem);
if (isEmbeddedEventExpr(itemp->sensp())) return true;
}
return false;
}
AstNodeExpr* newVarRead(FileLine* fl, AstVar* varp) {
return new AstVarRef{fl, varp, VAccess::READ};
}
AstNodeExpr* newEmbeddedVarNonNull(FileLine* fl) {
return new AstNeq{fl, new AstVarRef{fl, m_embeddedVarp, VAccess::READ},
new AstConst{fl, AstConst::Null{}}};
}
void installEmbeddedEventFork(AstSenTree* eventp) {
// Full clocking-event support for an embedded covergroup (IEEE 1800-2023 19.8.1):
// a coverage point is sampled the instant the clocking event takes place, on every
// occurrence, regardless of where the event signal is written. The covergroup is a
// dynamic per-instance object, so static sensitivity blocks (V3Active) cannot model
// this. Instead spawn, in the enclosing constructor right after construction, a
// persistent process:
// fork forever begin @(event); if (cgvar != null) cgvar.sample(); end join_none
// The event SenTree references enclosing-class members, which resolve directly against
// 'this' in the constructor. This requires --timing (fork + event control); the caller
// only reaches here when timing is enabled. 'eventp' is an unlinked SenTree template
// owned here.
const std::vector<AstNodeAssign*> constructps = findCovergroupConstructions();
for (AstNodeAssign* const constructp : constructps) {
FileLine* const fl = constructp->fileline();
AstLoop* const loopp = new AstLoop{fl};
loopp->addStmtsp(new AstEventControl{fl, eventp->cloneTree(false), nullptr});
loopp->addStmtsp(new AstIf{fl, newEmbeddedVarNonNull(fl), newSampleStmt(fl)});
AstFork* const forkp = new AstFork{fl, VJoinType::JOIN_NONE};
forkp->addForksp(new AstBegin{fl, "", loopp, true});
constructp->addNextHere(forkp);
}
VL_DO_DANGLING(pushDeletep(eventp), eventp);
}
AstNodeExpr* newEventReadyCondition(FileLine* fl, const EmbeddedEventTrigger& trigger) {
AstNodeExpr* const curp = newEventRead(fl, trigger);
AstNodeExpr* const prevp = newVarRead(fl, trigger.prevVarp);
AstNodeExpr* edgep = nullptr;
if (trigger.edgeType == VEdgeType::ET_POSEDGE) {
edgep = new AstLogAnd{fl, curp, new AstLogNot{fl, prevp}};
} else if (trigger.edgeType == VEdgeType::ET_NEGEDGE) {
edgep = new AstLogAnd{fl, new AstLogNot{fl, curp}, prevp};
} else {
edgep = new AstNeq{fl, curp, prevp};
}
return new AstLogAnd{fl,
new AstNeq{fl, new AstVarRef{fl, m_embeddedVarp, VAccess::READ},
new AstConst{fl, AstConst::Null{}}},
edgep};
}
AstNodeStmt* newSampleStmt(FileLine* fl) {
AstMethodCall* const callp = new AstMethodCall{
fl, new AstVarRef{fl, m_embeddedVarp, VAccess::READ}, "sample", nullptr};
callp->taskp(m_sampleFuncp);
callp->dtypeSetVoid();
return callp->makeStmt();
}
AstNodeStmt* newPrevUpdate(FileLine* fl, const EmbeddedEventTrigger& trigger) {
return new AstAssign{fl, new AstVarRef{fl, trigger.prevVarp, VAccess::WRITE},
newEventRead(fl, trigger)};
}
std::vector<EmbeddedEventTrigger> collectEmbeddedEventTriggers(AstCovergroup* cgp) {
std::vector<EmbeddedEventTrigger> triggers;
if (!m_enclosingClassp || !m_embeddedVarp || !cgp || !cgp->eventp()) return triggers;
std::vector<AstSenItem*> itemps;
for (AstNode* senp = cgp->eventp()->sensesp(); senp; senp = senp->nextp()) {
AstSenItem* const itemp = VN_AS(senp, SenItem);
if (!isEmbeddedEventExpr(itemp->sensp())) return {};
itemps.push_back(itemp);
}
for (AstSenItem* const itemp : itemps) {
AstVar* baseVarp = nullptr;
AstVar* memberVarp = nullptr;
UASSERT_OBJ(parseEmbeddedEventExpr(itemp->sensp(), baseVarp, memberVarp), itemp,
"Bad embedded covergroup event expression");
// Defer the previous-value member to installEmbeddedEventTriggers so it is only
// created once an in-class assignment to instrument is known to exist.
triggers.emplace_back(itemp->fileline(), baseVarp, memberVarp, itemp->edgeType(),
nullptr);
}
return triggers;
}
void installEmbeddedEventTriggers(std::vector<EmbeddedEventTrigger>& triggers) {
// An embedded covergroup is lowered into a sibling class, so a clocking event on an
// enclosing-class member cannot be placed in a static sensitivity list. Approximate
// it by sampling immediately after each blocking assignment to the event signal that
// appears textually within the enclosing class. This is only an approximation: it
// cannot observe changes made from outside the class (e.g. a testbench poking a public
// member) nor model the exact scheduling region of a real clocking event. When the
// signal is never assigned inside the class no sampling is possible at all, so warn
// rather than silently producing zero coverage (IEEE 1800-2023 19.4).
for (EmbeddedEventTrigger& trigger : triggers) {
std::vector<AstNodeAssign*> assignps;
m_enclosingClassp->foreach([&](AstNodeAssign* asgnp) {
if (isEventLvalue(asgnp->lhsp(), trigger)) assignps.push_back(asgnp);
});
if (assignps.empty()) {
trigger.eventFl->v3warn(
COVERIGN, "Unsupported: 'covergroup' clocking event signal has no assignment "
"within the enclosing class; no coverage sampled");
continue;
}
AstVar* const prevVarp = newEventPrevVar(trigger.eventFl, trigger);
m_enclosingClassp->addMembersp(prevVarp);
trigger.prevVarp = prevVarp;
for (AstNodeAssign* const asgnp : assignps) {
FileLine* const fl = asgnp->fileline();
AstIf* const ifp
= new AstIf{fl, newEventReadyCondition(fl, trigger), newSampleStmt(fl)};
ifp->addNextHere(newPrevUpdate(fl, trigger));
asgnp->addNextHere(ifp);
}
}
}
AstNode* findEnclosingMemberRef() {
// An embedded covergroup is lowered into a sibling AstClass that has no handle to // An embedded covergroup is lowered into a sibling AstClass that has no handle to
// the enclosing object. A coverpoint/iff/cross expression that references a // the enclosing object. A coverpoint/iff/cross/event expression that references a
// (non-static) member of the enclosing class therefore emits C++ that accesses the // (non-static) member of the enclosing class therefore emits C++ that accesses the
// member as if it were static ("invalid use of non-static data member"). Detect // member as if it were static ("invalid use of non-static data member"). Detect
// such references so the caller can skip lowering with a clean warning instead of // such references so the caller can skip lowering with a clean warning instead of
@ -1702,9 +1958,7 @@ class FunctionalCoverageVisitor final : public VNVisitor {
// Collect the covergroup class's own member variables (sample/constructor args); // Collect the covergroup class's own member variables (sample/constructor args);
// references to those are legitimate. // references to those are legitimate.
std::set<const AstVar*> ownVars; std::set<const AstVar*> ownVars;
for (AstNode* itemp = cgClassp->membersp(); itemp; itemp = itemp->nextp()) { collectOwnVars(ownVars);
if (const AstVar* const varp = VN_CAST(itemp, Var)) ownVars.insert(varp);
}
AstNode* offenderp = nullptr; AstNode* offenderp = nullptr;
const auto scan = [&](AstNode* rootp) { const auto scan = [&](AstNode* rootp) {
rootp->foreach([&](AstVarRef* refp) { rootp->foreach([&](AstVarRef* refp) {
@ -1715,27 +1969,152 @@ class FunctionalCoverageVisitor final : public VNVisitor {
// legitimate and excluded via ownVars. // legitimate and excluded via ownVars.
if (varp->isClassMember() && !ownVars.count(varp)) offenderp = refp; if (varp->isClassMember() && !ownVars.count(varp)) offenderp = refp;
}); });
rootp->foreach([&](AstThisRef* refp) {
if (offenderp) return;
const AstClassRefDType* const refDTypep
= VN_CAST(refp->dtypep()->skipRefp(), ClassRefDType);
if (refDTypep && refDTypep->classp() == m_covergroupp) offenderp = refp;
});
}; };
for (AstCoverpoint* cpp : m_coverpoints) scan(cpp); for (AstCoverpoint* cpp : m_coverpoints) scan(cpp);
for (AstCoverCross* crossp : m_coverCrosses) scan(crossp); for (AstCoverCross* crossp : m_coverCrosses) scan(crossp);
for (AstNode* itemp = m_covergroupp->membersp(); itemp; itemp = itemp->nextp()) {
if (AstCovergroup* const cgp = VN_CAST(itemp, Covergroup)) scan(cgp);
}
return offenderp; return offenderp;
} }
std::vector<AstNodeAssign*> findCovergroupConstructions() {
// IEEE 1800-2023 19.4: an embedded covergroup variable is constructed (and only
// constructed) in the enclosing class's new(), as 'cgvar = new'. Return all such
// assignments so each constructed instance receives its back-pointer.
std::vector<AstNodeAssign*> foundps;
if (!m_embeddedVarp) return foundps;
AstFunc* const enclosingNewp
= VN_CAST(m_memberMap.findMember(m_enclosingClassp, "new"), Func);
if (!enclosingNewp) return foundps;
enclosingNewp->foreach([&](AstNodeAssign* asgnp) {
const AstNew* const newp = VN_CAST(asgnp->rhsp(), New);
if (!newp) return;
const AstClassRefDType* const refp = VN_CAST(newp->dtypep(), ClassRefDType);
if (!refp || refp->classp() != m_covergroupp) return;
const AstVarRef* const lhsRefp = VN_CAST(asgnp->lhsp(), VarRef);
if (!lhsRefp || lhsRefp->varp() != m_embeddedVarp) return;
foundps.push_back(asgnp);
});
return foundps;
}
bool installEnclosingBackPointer() {
// Simple-case support for embedded covergroups (IEEE 1800-2023 19.4) whose
// coverpoints reference members of the enclosing class ("Class members can be used
// in coverpoint expressions"). The covergroup is lowered into a sibling class with
// no implicit handle to the enclosing object, so such references would emit
// uncompilable C++. Add an explicit back-pointer member to the enclosing instance,
// route the member references through it, and initialize it right after the
// 'cgvar = new' construction. The enclosing member values are only read in
// sample(), which runs after construction, so this ordering is safe. Returns true
// if the back-pointer was installed; false leaves the COVERIGN safety net to the
// caller for anything outside this simple form.
if (!m_enclosingClassp) return false; // Covergroup not embedded in a class
// Collect the covergroup's own members (sample/constructor args, prior generated
// members); references to those are legitimate and must not be rewritten.
std::set<const AstVar*> ownVars;
collectOwnVars(ownVars);
// Collect the enclosing class's inherited members; only references to these can be
// resolved through the back-pointer.
std::set<const AstVar*> enclosingVars;
collectEnclosingVars(enclosingVars);
// Gather the references to rewrite. Bail (keep the safety net) if any
// enclosing reference is not a direct member of the enclosing class, so a reference
// that cannot be resolved through the back-pointer is never silently rewritten.
std::vector<AstVarRef*> refsToRewrite;
std::vector<AstThisRef*> thisRefsToRewrite;
bool bail = false;
const auto scan = [&](AstNode* rootp) {
rootp->foreach([&](AstVarRef* refp) {
const AstVar* const varp = refp->varp();
if (!varp->isClassMember() || ownVars.count(varp)) return;
if (enclosingVars.count(varp)) {
refsToRewrite.push_back(refp);
} else {
bail = true;
}
});
rootp->foreach([&](AstThisRef* refp) {
const AstClassRefDType* const refDTypep
= VN_CAST(refp->dtypep()->skipRefp(), ClassRefDType);
if (refDTypep && refDTypep->classp() == m_covergroupp)
thisRefsToRewrite.push_back(refp);
});
};
for (AstCoverpoint* const cpp : m_coverpoints) scan(cpp);
for (AstCoverCross* const crossp : m_coverCrosses) scan(crossp);
for (AstNode* itemp = m_covergroupp->membersp(); itemp; itemp = itemp->nextp()) {
if (AstCovergroup* const cgp = VN_CAST(itemp, Covergroup)) scan(cgp);
}
if (bail || (refsToRewrite.empty() && thisRefsToRewrite.empty())) return false;
// The covergroup must be constructed in the enclosing class so the back-pointer can
// be initialized; bail if no construction is found.
const std::vector<AstNodeAssign*> constructps = findCovergroupConstructions();
if (constructps.empty()) return false;
// Commit: add the back-pointer member, rewrite the references, initialize the handle.
FileLine* const fl = m_covergroupp->fileline();
AstClassRefDType* const enclDTypep = new AstClassRefDType{fl, m_enclosingClassp, nullptr};
v3Global.rootp()->typeTablep()->addTypesp(enclDTypep);
AstVar* const handleVarp
= new AstVar{fl, VVarType::MEMBER, "__Vcg_enclosingp", enclDTypep};
handleVarp->isStatic(false);
m_covergroupp->addMembersp(handleVarp);
// Route each enclosing-member reference through the back-pointer: 'm' -> 'h.m'.
for (AstVarRef* const refp : refsToRewrite) { rewriteVarRef(refp, handleVarp); }
for (AstThisRef* const refp : thisRefsToRewrite) { rewriteThisRef(refp, handleVarp); }
// Initialize the handle right after construction: 'cgvar.__Vcg_enclosingp = this;'.
// The back-pointer must be a non-owning (weak) reference so it does not keep the
// enclosing object alive (the enclosing object owns the covergroup). There is no AST
// node for a weak class-handle literal, so emit the runtime helper VL_WEAK_REF; it
// contains no source identifiers, so '--protect' is unaffected.
for (AstNodeAssign* const constructp : constructps) {
FileLine* const cfl = constructp->fileline();
AstMemberSel* const lhsp
= new AstMemberSel{cfl, constructp->lhsp()->cloneTree(false), handleVarp};
lhsp->access(VAccess::WRITE);
AstCExpr* const weakThisp = new AstCExpr{cfl};
weakThisp->add("VL_WEAK_REF(this)");
weakThisp->dtypep(enclDTypep);
constructp->addNextHere(new AstAssign{cfl, lhsp, weakThisp});
}
return true;
}
void visit(AstClass* nodep) override { void visit(AstClass* nodep) override {
UINFO(9, "Visiting class: " << nodep->name() << " isCovergroup=" << nodep->isCovergroup()); UINFO(9, "Visiting class: " << nodep->name() << " isCovergroup=" << nodep->isCovergroup());
if (nodep->isCovergroup()) { if (nodep->isCovergroup()) {
VL_RESTORER(m_covergroupp); VL_RESTORER(m_covergroupp);
VL_RESTORER(m_embeddedVarp);
VL_RESTORER(m_sampleFuncp); VL_RESTORER(m_sampleFuncp);
VL_RESTORER(m_constructorp); VL_RESTORER(m_constructorp);
VL_RESTORER(m_coverpoints); VL_RESTORER(m_coverpoints);
VL_RESTORER(m_coverpointMap); VL_RESTORER(m_coverpointMap);
VL_RESTORER(m_coverCrosses); VL_RESTORER(m_coverCrosses);
m_covergroupp = nodep; m_covergroupp = nodep;
m_embeddedVarp = nullptr;
m_sampleFuncp = nullptr; m_sampleFuncp = nullptr;
m_constructorp = nullptr; m_constructorp = nullptr;
m_coverpoints.clear(); m_coverpoints.clear();
m_coverpointMap.clear(); m_coverpointMap.clear();
m_coverCrosses.clear(); m_coverCrosses.clear();
m_embeddedVarp = findEmbeddedCovergroupVar();
std::vector<EmbeddedEventTrigger> embeddedEventTriggers;
// Clocking event referencing an enclosing member, lowered via a forked sampling
// process when --timing is available (full support); null otherwise.
AstSenTree* embeddedEventForkp = nullptr;
// Extract and store the clocking event from AstCovergroup node // Extract and store the clocking event from AstCovergroup node
// The parser creates this node to preserve the event information // The parser creates this node to preserve the event information
@ -1748,33 +2127,36 @@ class FunctionalCoverageVisitor final : public VNVisitor {
// event exists, so cgp->eventp() is always non-null here. // event exists, so cgp->eventp() is always non-null here.
UASSERT_OBJ(cgp->eventp(), cgp, UASSERT_OBJ(cgp->eventp(), cgp,
"Sentinel AstCovergroup in class must have non-null eventp"); "Sentinel AstCovergroup in class must have non-null eventp");
// Check if the clocking event references a member variable (unsupported) if (hasEmbeddedEventExpr(cgp)) {
// Clocking events should be on signals/nets, not class members // The clocking event references a member of the enclosing class, so it
bool eventUnsupported = false; // names a per-instance signal that V3Active's static-sensitivity path
for (AstNode* senp = cgp->eventp()->sensesp(); senp; senp = senp->nextp()) { // cannot express. Under --timing, lower it fully: a forked
AstSenItem* const senItemp = VN_AS(senp, SenItem); // 'forever @(event) sample()' process spawned in the enclosing
if (AstVarRef* const varrefp // LCOV_EXCL_BR_LINE // constructor samples on every occurrence of the event regardless of
= VN_CAST(senItemp->sensp(), VarRef)) { // where the signal is written (IEEE 1800-2023 19.8.1). Without
if (varrefp->varp()->isClassMember()) { // --timing, dynamic per-instance event waits are unavailable, so fall
cgp->v3warn(COVERIGN, "Unsupported: 'covergroup' clocking event " // back to a best-effort instrumentation of in-class assignments.
"on member variable"); if (v3Global.opt.timing().isSetTrue()) {
eventUnsupported = true; embeddedEventForkp = cgp->eventp()->unlinkFrBack();
} else {
embeddedEventTriggers = collectEmbeddedEventTriggers(cgp);
if (embeddedEventTriggers.empty()) {
cgp->v3warn(COVERIGN,
"Unsupported: 'covergroup' clocking event on complex "
"member expression; use --timing for full support");
hasUnsupportedEvent = true; hasUnsupportedEvent = true;
break;
} }
} }
} VL_DO_DANGLING(pushDeletep(cgp->unlinkFrBack()), cgp);
if (!eventUnsupported) {
// Leave cgp in the class membersp so the SenTree stays
// linked in the AST. V3Active will find it via membersp,
// use the event, then delete the AstCovergroup itself.
UINFO(4, "Keeping covergroup event node for V3Active: " << nodep->name());
itemp = nextp; itemp = nextp;
continue; continue;
} }
// Remove the AstCovergroup node - either unsupported event or no event // Event references only static/global signals. Leave cgp in the class
VL_DO_DANGLING(pushDeletep(cgp->unlinkFrBack()), cgp); // membersp so the SenTree stays linked in the AST. V3Active will find it
// via membersp, use the event, then delete the AstCovergroup itself.
UINFO(4, "Keeping covergroup event node for V3Active: " << nodep->name());
itemp = nextp;
continue;
} }
itemp = nextp; itemp = nextp;
} }
@ -1804,24 +2186,33 @@ class FunctionalCoverageVisitor final : public VNVisitor {
iterateChildren(nodep); iterateChildren(nodep);
// Option B safety net for embedded covergroups: if a coverpoint/iff/cross // Embedded covergroups (IEEE 1800-2023 19.4): coverage constructs may reference
// references a member of the enclosing class, lowering would emit uncompilable // members of the enclosing class. The covergroup is lowered into a sibling
// C++ (no handle to the enclosing instance). Skip this covergroup with a clean // class with no implicit handle to the enclosing instance, so install an
// warning rather than crashing the C++ compile. (Full support - an enclosing // explicit back-pointer and route the references through it. For anything
// back-pointer - is the planned follow-up.) // outside that form, fall back to the COVERIGN safety net:
if (AstNode* const offenderp = findEnclosingMemberRef(nodep)) { // skip the covergroup with a clean warning rather than emitting uncompilable C++.
offenderp->v3warn(COVERIGN, if (AstNode* const offenderp = findEnclosingMemberRef()) {
"Unsupported: 'covergroup' coverpoint referencing enclosing " if (!installEnclosingBackPointer()) {
"class member; ignoring covergroup " offenderp->v3warn(COVERIGN,
<< nodep->prettyNameQ()); "Unsupported: 'covergroup' coverage construct referencing "
for (AstCoverpoint* cpp : m_coverpoints) { "enclosing class member; ignoring covergroup "
VL_DO_DANGLING(pushDeletep(cpp->unlinkFrBack()), cpp); << nodep->prettyNameQ());
for (AstCoverpoint* cpp : m_coverpoints) {
VL_DO_DANGLING(pushDeletep(cpp->unlinkFrBack()), cpp);
}
for (AstCoverCross* crossp : m_coverCrosses) {
VL_DO_DANGLING(pushDeletep(crossp->unlinkFrBack()), crossp);
}
// The covergroup is ignored, so its sampling process is moot.
if (embeddedEventForkp) {
VL_DO_DANGLING(pushDeletep(embeddedEventForkp), embeddedEventForkp);
}
return;
} }
for (AstCoverCross* crossp : m_coverCrosses) {
VL_DO_DANGLING(pushDeletep(crossp->unlinkFrBack()), crossp);
}
return;
} }
installEmbeddedEventTriggers(embeddedEventTriggers);
if (embeddedEventForkp) installEmbeddedEventFork(embeddedEventForkp);
processCovergroup(); processCovergroup();
// Remove lowered coverpoints/crosses from the class - they have been // Remove lowered coverpoints/crosses from the class - they have been
@ -1833,6 +2224,10 @@ class FunctionalCoverageVisitor final : public VNVisitor {
VL_DO_DANGLING(pushDeletep(crossp->unlinkFrBack()), crossp); VL_DO_DANGLING(pushDeletep(crossp->unlinkFrBack()), crossp);
} }
} else { } else {
// Track the lexically enclosing class so a nested covergroup can resolve
// references to the enclosing object's members (installEnclosingBackPointer).
VL_RESTORER(m_enclosingClassp);
m_enclosingClassp = nodep;
iterateChildren(nodep); iterateChildren(nodep);
} }
} }

View File

@ -249,8 +249,37 @@ class EmitCHeader final : public EmitCConstInit {
if (const AstClass* const classp = VN_CAST(modp, Class)) { if (const AstClass* const classp = VN_CAST(modp, Class)) {
if (!classp->isInterfaceClass() && !classp->isVirtual()) { if (!classp->isInterfaceClass() && !classp->isVirtual()) {
decorateFirst(first, section); decorateFirst(first, section);
putns(classp, "VlClass* clone() const { return new " std::vector<const AstVar*> embeddedCovergroupVars;
+ EmitCUtil::prefixNameProtect(classp) + "(*this); }\n"); std::vector<AstClass*> classes;
for (AstClass* scanClassp = const_cast<AstClass*>(classp); scanClassp;
scanClassp = scanClassp->extendsp() ? scanClassp->extendsp()->classp()
: nullptr) {
classes.push_back(scanClassp);
}
for (auto it = classes.rbegin(); it != classes.rend(); ++it) {
AstClass* const memberClassp = *it;
for (AstNode* stmtp = memberClassp->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
AstVar* const varp = VN_CAST(stmtp, Var);
if (!varp) continue;
const AstClassRefDType* const refp
= VN_CAST(varp->dtypep()->skipRefp(), ClassRefDType);
if (refp && refp->classp()->isCovergroup()) {
embeddedCovergroupVars.push_back(varp);
}
}
}
const string className = EmitCUtil::prefixNameProtect(classp);
if (embeddedCovergroupVars.empty()) {
putns(classp, "VlClass* clone() const { return new " + className
+ "(*this); }\n");
} else {
putns(classp, "VlClass* clone() const { " + className
+ "* const clonep = new " + className + "(*this); ");
for (const AstVar* const varp : embeddedCovergroupVars) {
puts("clonep->" + varp->nameProtect() + " = VlNull{}; ");
}
puts("return clonep; }\n");
}
} }
} }
} }

View File

@ -3437,6 +3437,16 @@ class LinkDotResolveVisitor final : public VNVisitor {
} }
return classSymp; return classSymp;
} }
VSymEnt* enclosingClassSympForCovergroup(VSymEnt* classSymp) {
if (!classSymp) return nullptr;
const AstClass* const classp = VN_CAST(classSymp->nodep(), Class);
if (!classp || !classp->isCovergroup()) return nullptr;
VSymEnt* parentSymp = classSymp->parentp();
while (parentSymp && !VN_IS(parentSymp->nodep(), Class)) {
parentSymp = parentSymp->parentp();
}
return parentSymp;
}
void importDerivedClass(AstClass* derivedClassp, VSymEnt* baseSymp, AstClass* baseClassp) { void importDerivedClass(AstClass* derivedClassp, VSymEnt* baseSymp, AstClass* baseClassp) {
// Also used for standard 'extends' from a base class // Also used for standard 'extends' from a base class
UINFO(8, indent() << "importDerivedClass to " << derivedClassp << " from " << baseClassp); UINFO(8, indent() << "importDerivedClass to " << derivedClassp << " from " << baseClassp);
@ -4313,6 +4323,14 @@ class LinkDotResolveVisitor final : public VNVisitor {
} else { } else {
foundp = m_ds.m_dotSymp->findIdFlat(nodep->name()); foundp = m_ds.m_dotSymp->findIdFlat(nodep->name());
} }
if (!foundp && m_ds.m_dotp && VN_IS(m_ds.m_dotp->lhsp(), ParseRef)
&& m_ds.m_dotp->lhsp()->name() == "this") {
if (VSymEnt* const parentClassSymp
= enclosingClassSympForCovergroup(m_ds.m_dotSymp)) {
foundp = parentClassSymp->findIdFallback(nodep->name());
if (foundp) m_ds.m_dotSymp = parentClassSymp;
}
}
// If not found in modport, check interface fallback for parameters and typedefs. // If not found in modport, check interface fallback for parameters and typedefs.
// Parameters and typedefs are always visible through a modport (IEEE 1800-2023 25.5). // Parameters and typedefs are always visible through a modport (IEEE 1800-2023 25.5).
// This mirrors the VarXRef modport parameter fallback in visit(AstVarXRef). // This mirrors the VarXRef modport parameter fallback in visit(AstVarXRef).

View File

@ -45,8 +45,8 @@ def covergroup_coverage_report(test, outfile=None):
return outfile return outfile
def run(test, *, verilator_flags2=()): def run(test, *, verilator_flags2=(), timing_loop=False):
test.compile(verilator_flags2=['--coverage', *verilator_flags2]) test.compile(verilator_flags2=['--coverage', *verilator_flags2], timing_loop=timing_loop)
test.execute() test.execute()
covergroup_coverage_report(test) covergroup_coverage_report(test)
test.files_identical(test.obj_dir + '/covergroup_report.txt', test.golden_filename) test.files_identical(test.obj_dir + '/covergroup_report.txt', test.golden_filename)

View File

@ -0,0 +1,30 @@
__vlAnonCG_branch_cg.cp_branch.hi: 8
__vlAnonCG_branch_cg.cp_branch.lo: 8
__vlAnonCG_clock_cg.cp_clocked.hi: 8
__vlAnonCG_clock_cg.cp_clocked.lo: 8
__vlAnonCG_copy_cg.cp_copy.hi: 0
__vlAnonCG_copy_cg.cp_copy.lo: 1
__vlAnonCG_derived_cg.cp_inherited.hi: 8
__vlAnonCG_derived_cg.cp_inherited.lo: 8
__vlAnonCG_mon_cg.addr_x_op_b.hi_x_auto_0 [cross]: 2
__vlAnonCG_mon_cg.addr_x_op_b.hi_x_auto_1 [cross]: 2
__vlAnonCG_mon_cg.addr_x_op_b.hi_x_auto_2 [cross]: 2
__vlAnonCG_mon_cg.addr_x_op_b.hi_x_auto_3 [cross]: 2
__vlAnonCG_mon_cg.addr_x_op_b.lo_x_auto_0 [cross]: 2
__vlAnonCG_mon_cg.addr_x_op_b.lo_x_auto_1 [cross]: 2
__vlAnonCG_mon_cg.addr_x_op_b.lo_x_auto_2 [cross]: 2
__vlAnonCG_mon_cg.addr_x_op_b.lo_x_auto_3 [cross]: 2
__vlAnonCG_mon_cg.cp_addr.hi: 8
__vlAnonCG_mon_cg.cp_addr.lo: 8
__vlAnonCG_mon_cg.cp_enabled.hi: 8
__vlAnonCG_mon_cg.cp_enabled.lo: 0
__vlAnonCG_mon_cg.cp_inner.hi: 8
__vlAnonCG_mon_cg.cp_inner.lo: 8
__vlAnonCG_mon_cg.cp_op_a.hi: 8
__vlAnonCG_mon_cg.cp_op_a.lo: 8
__vlAnonCG_mon_cg.cp_op_b.auto_0: 4
__vlAnonCG_mon_cg.cp_op_b.auto_1: 4
__vlAnonCG_mon_cg.cp_op_b.auto_2: 4
__vlAnonCG_mon_cg.cp_op_b.auto_3: 4
__vlAnonCG_this_cg.cp_this.hi: 8
__vlAnonCG_this_cg.cp_this.lo: 8

View File

@ -9,8 +9,8 @@
import vltest_bootstrap import vltest_bootstrap
import coverage_covergroup_common
test.scenarios('vlt') test.scenarios('vlt')
test.lint(expect_filename=test.golden_filename, fails=True) coverage_covergroup_common.run(test)
test.passes()

View File

@ -0,0 +1,197 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain, for
// any use, without warranty, 2026 by Wilson Snyder.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Embedded covergroups whose coverage constructs reference members of the
// enclosing class. IEEE 1800-2023 19.4 allows class members in coverpoint
// expressions, conditional guards, option initialization, and other coverage
// constructs; 8.11 also allows 'this' within an embedded covergroup.
// verilog_format: off
`define stop $stop
`define checkd(gotv, expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__, `__LINE__, (gotv), (expv)); `stop; end while (0);
// verilog_format: on
class Inner;
bit [3:0] value;
endclass
class Transaction;
bit [7:0] operand_a;
bit [1:0] operand_b;
Inner inner;
endclass
class Monitor;
bit [3:0] addr; // Direct member of the enclosing class
bit enable;
Transaction trx; // Class-handle member of the enclosing class
covergroup mon_cg;
cp_addr: coverpoint addr {bins lo = {[0 : 7]}; bins hi = {[8 : 15]};}
cp_enabled: coverpoint addr iff (enable) {bins lo = {[0 : 7]}; bins hi = {[8 : 15]};}
cp_op_a: coverpoint trx.operand_a {bins lo = {[0 : 127]}; bins hi = {[128 : 255]};}
cp_op_b: coverpoint trx.operand_b;
cp_inner: coverpoint trx.inner.value {bins lo = {[0 : 7]}; bins hi = {[8 : 15]};}
addr_x_op_b: cross cp_addr, cp_op_b;
endgroup
function new();
trx = new;
trx.inner = new;
mon_cg = new;
endfunction
function void observe(bit [3:0] a, bit [7:0] oa, bit [1:0] ob, bit [3:0] iv);
addr = a;
enable = a[3];
trx.operand_a = oa;
trx.operand_b = ob;
trx.inner.value = iv;
mon_cg.sample();
endfunction
endclass
class BranchMonitor;
bit [2:0] value;
covergroup branch_cg;
cp_branch: coverpoint value {bins lo = {[0 : 3]}; bins hi = {[4 : 7]};}
endgroup
function new(bit choose_first);
if (choose_first) begin
branch_cg = new;
end
else begin
branch_cg = new;
end
endfunction
function void observe(bit [2:0] v);
value = v;
branch_cg.sample();
endfunction
endclass
class BaseMonitor;
bit [3:0] inherited_value;
endclass
class DerivedMonitor extends BaseMonitor;
covergroup derived_cg;
cp_inherited: coverpoint inherited_value {bins lo = {[0 : 7]}; bins hi = {[8 : 15]};}
endgroup
function new();
derived_cg = new;
endfunction
function void observe(bit [3:0] v);
inherited_value = v;
derived_cg.sample();
endfunction
endclass
class ThisMonitor;
bit [3:0] current;
covergroup this_cg;
cp_this: coverpoint this.current {bins lo = {[0 : 7]}; bins hi = {[8 : 15]};}
endgroup
function new();
this_cg = new;
endfunction
function void observe(bit [3:0] v);
current = v;
this_cg.sample();
endfunction
endclass
class ClockEvent;
bit clk;
endclass
class ClockMonitor;
ClockEvent ev;
bit [3:0] sampled;
covergroup clock_cg @(posedge ev.clk);
cp_clocked: coverpoint sampled {bins lo = {[0 : 7]}; bins hi = {[8 : 15]};}
endgroup
function new();
ev = new;
ev.clk = 0;
clock_cg = new;
endfunction
function void observe(bit [3:0] v);
sampled = v;
ev.clk = 0;
ev.clk = 1;
endfunction
endclass
class CopyMonitor;
bit [3:0] value;
covergroup copy_cg;
cp_copy: coverpoint value {bins lo = {[0 : 7]}; bins hi = {[8 : 15]};}
endgroup
function new();
copy_cg = new;
endfunction
function void observe(bit [3:0] v);
value = v;
copy_cg.sample();
endfunction
endclass
module t;
Monitor mon;
BranchMonitor branch_a;
BranchMonitor branch_b;
DerivedMonitor derived;
ThisMonitor this_mon;
ClockMonitor clock_mon;
CopyMonitor copy_src;
CopyMonitor copy_dst;
int i;
initial begin
mon = new;
branch_a = new(1);
branch_b = new(0);
derived = new;
this_mon = new;
clock_mon = new;
copy_src = new;
for (i = 0; i < 16; ++i) begin
mon.observe(i[3:0], i[7:0] * 17, i[1:0], i[3:0]);
derived.observe(i[3:0]);
this_mon.observe(i[3:0]);
clock_mon.observe(i[3:0]);
end
for (i = 0; i < 8; ++i) begin
branch_a.observe(i[2:0]);
branch_b.observe(i[2:0]);
end
copy_src.observe(4'h1);
copy_dst = new copy_src;
`checkd(copy_dst.copy_cg == null, 1);
$write("*-* All Finished *-*\n");
$finish;
end
endmodule

View File

@ -0,0 +1,4 @@
__vlAnonCG_chain_cg.cp_val.hi: 4
__vlAnonCG_chain_cg.cp_val.lo: 3
__vlAnonCG_ext_cg.cp_val.hi: 8
__vlAnonCG_ext_cg.cp_val.lo: 0

View File

@ -0,0 +1,19 @@
#!/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')
# --timing enables full embedded-covergroup clocking-event support: the covergroup is
# sampled on every occurrence of the event, including events driven from outside the
# enclosing class and complex member-chain events.
coverage_covergroup_common.run(test, verilator_flags2=['--timing'], timing_loop=True)

View File

@ -0,0 +1,90 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain, for
// any use, without warranty, 2026 by Wilson Snyder.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Full clocking-event support for embedded covergroups (IEEE 1800-2023 19.8.1):
// with --timing a covergroup is sampled on every occurrence of its clocking event,
// regardless of where the event signal is written. This exercises events driven
// from OUTSIDE the enclosing class (impossible to model without --timing) and a
// complex member-chain event, both of which the --no-timing path cannot sample.
class ExtClkMonitor;
bit clk; // Clocking-event signal driven only from outside the class
bit [3:0] value;
covergroup ext_cg @(posedge clk);
cp_val: coverpoint value {bins lo = {[0 : 7]}; bins hi = {[8 : 15]};}
endgroup
function new();
ext_cg = new;
endfunction
endclass
class Lvl;
bit ev;
endclass
class Mid;
Lvl lvl;
endclass
class ChainMonitor;
bit a; // First clocking term, driven externally
Mid mid; // Second clocking term reached via a member chain, driven externally
bit [3:0] value;
covergroup chain_cg @(posedge a or posedge mid.lvl.ev);
cp_val: coverpoint value {bins lo = {[0 : 7]}; bins hi = {[8 : 15]};}
endgroup
function new();
mid = new;
mid.lvl = new;
chain_cg = new;
endfunction
endclass
module t;
ExtClkMonitor extm;
ChainMonitor chain;
int i;
initial begin
extm = new;
chain = new;
// Drive the clocking event from OUTSIDE the class: 8 external posedges, all
// sampling the 'hi' range -> cp_value.hi = 8, cp_value.lo = 0.
for (i = 0; i < 8; ++i) begin
extm.value = 4'h8 | i[3:0];
extm.clk = 1'b0;
#1;
extm.clk = 1'b1;
#1;
end
// Complex member-chain event: 4 posedges on 'a' (hi range) and 3 posedges on
// 'mid.lvl.ev' (lo range), all driven externally -> cp_value.hi = 4, .lo = 3.
for (i = 0; i < 4; ++i) begin
chain.value = 4'hC;
chain.a = 1'b0;
#1;
chain.a = 1'b1;
#1;
end
for (i = 0; i < 3; ++i) begin
chain.value = 4'h2;
chain.mid.lvl.ev = 1'b0;
#1;
chain.mid.lvl.ev = 1'b1;
#1;
end
#1;
$write("*-* All Finished *-*\n");
$finish;
end
endmodule

View File

@ -1,7 +0,0 @@
%Warning-COVERIGN: t/t_covergroup_embedded_unsup.v:27:34: Unsupported: 'covergroup' coverpoint referencing enclosing class member; ignoring covergroup '__vlAnonCG_cov_trans'
: ... note: In instance 't'
27 | trans_start_addr: coverpoint trans_collected.addr {option.auto_bin_max = 16;}
| ^~~~~~~~~~~~~~~
... For warning description see https://verilator.org/warn/COVERIGN?v=latest
... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message.
%Error: Exiting due to

View File

@ -1,45 +0,0 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain, for
// any use, without warranty, 2026 by Wilson Snyder.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
// Test the graceful-degradation safety net for embedded covergroups (the dominant
// UVM pattern: a covergroup declared inside a class whose coverpoints reference the
// enclosing object's members). Such a covergroup is lowered into a sibling class
// with no handle to the enclosing instance, so emitting it would produce
// uncompilable C++ ("invalid use of non-static data member"). Until the enclosing
// back-pointer feature exists, Verilator must emit a clean COVERIGN warning and skip
// lowering the covergroup, rather than crashing the C++ compile.
class ubus_transfer;
bit [15:0] addr;
bit read_write;
endclass
class ubus_master_monitor;
ubus_transfer trans_collected;
// Coverpoints reference 'trans_collected', a member of the enclosing class.
// A cross is included so the safety-net cleanup also exercises cross removal.
covergroup cov_trans;
trans_start_addr: coverpoint trans_collected.addr {option.auto_bin_max = 16;}
trans_dir: coverpoint trans_collected.read_write;
trans_addr_x_dir : cross trans_start_addr, trans_dir;
endgroup
function new();
trans_collected = new;
cov_trans = new;
endfunction
endclass
module t;
ubus_master_monitor m;
initial begin
m = new;
$write("*-* All Finished *-*\n");
$finish;
end
endmodule

View File

@ -1,7 +1,14 @@
%Warning-COVERIGN: t/t_covergroup_member_event_unsup.v:13:5: Unsupported: 'covergroup' clocking event on member variable %Warning-COVERIGN: t/t_covergroup_member_event_unsup.v:19:18: Unsupported: 'covergroup' coverage construct referencing enclosing class member; ignoring covergroup '__vlAnonCG_cov_nonew'
: ... note: In instance 't' : ... note: In instance 't'
13 | covergroup cov1 @m_z; 19 | coverpoint m_x {bins lo = {[0 : 7]}; bins hi = {[8 : 15]};}
| ^~~~~~~~~~ | ^~~
... For warning description see https://verilator.org/warn/COVERIGN?v=latest ... For warning description see https://verilator.org/warn/COVERIGN?v=latest
... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message.
%Warning-COVERIGN: t/t_covergroup_member_event_unsup.v:28:29: Unsupported: 'covergroup' clocking event signal has no assignment within the enclosing class; no coverage sampled
28 | covergroup cov_extclk @(posedge clk);
| ^~~~~~~
%Warning-COVERIGN: t/t_covergroup_member_event_unsup.v:48:5: Unsupported: 'covergroup' clocking event on complex member expression; use --timing for full support
: ... note: In instance 't'
48 | covergroup cov_cplx @(posedge a or posedge mid.lvl.ev);
| ^~~~~~~~~~
%Error: Exiting due to %Error: Exiting due to

View File

@ -11,6 +11,10 @@ import vltest_bootstrap
test.scenarios('vlt') test.scenarios('vlt')
test.lint(expect_filename=test.golden_filename, fails=True) # Without --timing, an embedded covergroup clocked on (or otherwise referencing) an
# enclosing-class member cannot be fully supported: dynamic per-instance event waits are
# unavailable. Verilator emits a clean COVERIGN warning for each such limitation. With
# --timing these same covergroups are fully supported (see t_covergroup_embedded_timing).
test.lint(verilator_flags2=['--no-timing'], expect_filename=test.golden_filename, fails=True)
test.passes() test.passes()

View File

@ -4,17 +4,65 @@
// SPDX-FileCopyrightText: 2024 Wilson Snyder // SPDX-FileCopyrightText: 2024 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0 // SPDX-License-Identifier: CC0-1.0
module t ( // Embedded covergroups (IEEE 1800-2023 19.4) whose clocking event or coverage
input clk // constructs reference enclosing-class members in ways Verilator cannot fully
); // support. Each must emit a clean COVERIGN warning rather than silently
class Packet; // producing wrong coverage or uncompilable C++.
int m_z;
int m_x; module t;
covergroup cov1 @m_z; // Coverpoint references an enclosing member, but the covergroup is never
coverpoint m_x; // constructed (no 'new'), so it cannot be lowered through a back-pointer.
class NoConstruct;
bit [3:0] m_x;
bit m_z;
covergroup cov_nonew @m_z;
coverpoint m_x {bins lo = {[0 : 7]}; bins hi = {[8 : 15]};}
endgroup endgroup
endclass endclass
// Clocking event on a member that is never assigned inside the class, so no
// in-class sampling point exists (e.g. the signal is driven only externally).
class ExternalClk;
bit clk;
bit [3:0] m_x;
covergroup cov_extclk @(posedge clk);
coverpoint m_x {bins lo = {[0 : 7]}; bins hi = {[8 : 15]};}
endgroup
function new();
cov_extclk = new;
endfunction
endclass
// Clocking event mixing a member signal with a complex member chain that the
// member-event lowering cannot represent.
class Lvl;
bit ev;
endclass
class Mid;
Lvl lvl;
endclass
class ComplexEvt;
bit a;
Mid mid;
bit [3:0] m_x;
covergroup cov_cplx @(posedge a or posedge mid.lvl.ev);
coverpoint m_x {bins lo = {[0 : 7]}; bins hi = {[8 : 15]};}
endgroup
function new();
mid = new;
mid.lvl = new;
cov_cplx = new;
endfunction
endclass
NoConstruct nc;
ExternalClk ec;
ComplexEvt cx;
initial begin initial begin
nc = new;
ec = new;
cx = new;
$write("*-* All Finished *-*\n"); $write("*-* All Finished *-*\n");
$finish; $finish;
end end

View File

@ -300,7 +300,7 @@
: ... note: In instance 't' : ... note: In instance 't'
177 | cross a, b { 177 | cross a, b {
| ^ | ^
%Warning-COVERIGN: t/t_covergroup_unsup.v:209:5: Unsupported: 'covergroup' clocking event on member variable %Warning-COVERIGN: t/t_covergroup_unsup.v:210:24: Unsupported: 'covergroup' coverage construct referencing enclosing class member; ignoring covergroup '__vlAnonCG_cov1'
: ... note: In instance 't' : ... note: In instance 't'
209 | covergroup cov1 @m_z; 210 | cp_x: coverpoint m_x;
| ^~~~~~~~~~ | ^~~