diff --git a/include/verilated_types.h b/include/verilated_types.h index a48b99c4c..5d8c249a8 100644 --- a/include/verilated_types.h +++ b/include/verilated_types.h @@ -2103,15 +2103,16 @@ private: // MEMBERS T_Class* m_objp = nullptr; // Object pointed to + bool m_weak = false; // Non-owning reference // METHODS // Increase reference counter with null check 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 void refCountDec() const VL_MT_SAFE { - if (m_objp) m_objp->refCountDec(); + if (m_objp && !m_weak) m_objp->refCountDec(); } public: @@ -2119,7 +2120,7 @@ public: VlClassRef() = default; // Init with nullptr // cppcheck-suppress noExplicitConstructor - VlClassRef(VlNull){}; + VlClassRef(VlNull) {}; template VlClassRef(VlDeleter& deleter, T_Args&&... args) : m_objp{new T_Class} { @@ -2151,58 +2152,79 @@ public: } // cppcheck-suppress noExplicitConstructor VlClassRef(const VlClassRef& copied) - : m_objp{copied.m_objp} { + : m_objp{copied.m_objp} + , m_weak{copied.m_weak} { refCountInc(); } // cppcheck-suppress noExplicitConstructor 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 template VlClassRef(const VlClassRef& copied) - : m_objp{copied.m_objp} { + : m_objp{copied.m_objp} + , m_weak{copied.m_weak} { refCountInc(); } // cppcheck-suppress noExplicitConstructor template 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)} {} ~VlClassRef() { refCountDec(); } // 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 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(); m_objp = copied.m_objp; + m_weak = copied.m_weak; refCountInc(); return *this; } 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(); m_objp = std::exchange(moved.m_objp, nullptr); + m_weak = std::exchange(moved.m_weak, false); return *this; } template 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(); m_objp = copied.m_objp; + m_weak = copied.m_weak; refCountInc(); return *this; } template 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(); m_objp = std::exchange(moved.m_objp, nullptr); + m_weak = std::exchange(moved.m_weak, false); return *this; } // Assign with nullptr VlClassRef& operator=(VlNull) { refCountDec(); m_objp = nullptr; + m_weak = false; return *this; } // Dynamic caster @@ -2253,6 +2275,14 @@ static inline bool VL_CAST_DYNAMIC(VlClassRef in, VlClassRef& outr 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 +static inline VlClassRef VL_WEAK_REF(T_Class* objp) { + return VlClassRef::weak(objp); +} + template static inline bool VL_CAST_DYNAMIC(VlNull, VlClassRef& outr) { outr = VlNull{}; diff --git a/src/V3Covergroup.cpp b/src/V3Covergroup.cpp index b1890e001..2114422cb 100644 --- a/src/V3Covergroup.cpp +++ b/src/V3Covergroup.cpp @@ -30,6 +30,7 @@ #include "V3MemberMap.h" #include +#include VL_DEFINE_DEBUG_FUNCTIONS; @@ -44,6 +45,8 @@ class FunctionalCoverageVisitor final : public VNVisitor { // STATE 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_constructorp = nullptr; // Current constructor std::vector m_coverpoints; // Coverpoints in current covergroup @@ -68,6 +71,22 @@ class FunctionalCoverageVisitor final : public VNVisitor { , crossBins{cb} {} }; std::vector 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 m_crossedCpNames; // Coverpoints referenced by a cross (kept legacy) std::vector m_convCpVars; // VlCoverpoint members of converted coverpoints AstCDType* m_vlCoverpointDTypep = nullptr; // Shared "VlCoverpoint" C++ member type @@ -1692,9 +1711,246 @@ class FunctionalCoverageVisitor final : public VNVisitor { } // VISITORS - AstNode* findEnclosingMemberRef(AstClass* cgClassp) { + void collectOwnVars(std::set& 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& 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 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 collectEmbeddedEventTriggers(AstCovergroup* cgp) { + std::vector triggers; + if (!m_enclosingClassp || !m_embeddedVarp || !cgp || !cgp->eventp()) return triggers; + + std::vector 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& 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 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 - // 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 // 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 @@ -1702,9 +1958,7 @@ class FunctionalCoverageVisitor final : public VNVisitor { // Collect the covergroup class's own member variables (sample/constructor args); // references to those are legitimate. std::set ownVars; - for (AstNode* itemp = cgClassp->membersp(); itemp; itemp = itemp->nextp()) { - if (const AstVar* const varp = VN_CAST(itemp, Var)) ownVars.insert(varp); - } + collectOwnVars(ownVars); AstNode* offenderp = nullptr; const auto scan = [&](AstNode* rootp) { rootp->foreach([&](AstVarRef* refp) { @@ -1715,27 +1969,152 @@ class FunctionalCoverageVisitor final : public VNVisitor { // legitimate and excluded via ownVars. 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 (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; } + std::vector 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 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 ownVars; + collectOwnVars(ownVars); + // Collect the enclosing class's inherited members; only references to these can be + // resolved through the back-pointer. + std::set 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 refsToRewrite; + std::vector 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 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 { UINFO(9, "Visiting class: " << nodep->name() << " isCovergroup=" << nodep->isCovergroup()); if (nodep->isCovergroup()) { VL_RESTORER(m_covergroupp); + VL_RESTORER(m_embeddedVarp); VL_RESTORER(m_sampleFuncp); VL_RESTORER(m_constructorp); VL_RESTORER(m_coverpoints); VL_RESTORER(m_coverpointMap); VL_RESTORER(m_coverCrosses); m_covergroupp = nodep; + m_embeddedVarp = nullptr; m_sampleFuncp = nullptr; m_constructorp = nullptr; m_coverpoints.clear(); m_coverpointMap.clear(); m_coverCrosses.clear(); + m_embeddedVarp = findEmbeddedCovergroupVar(); + std::vector 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 // 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. UASSERT_OBJ(cgp->eventp(), cgp, "Sentinel AstCovergroup in class must have non-null eventp"); - // Check if the clocking event references a member variable (unsupported) - // Clocking events should be on signals/nets, not class members - bool eventUnsupported = false; - for (AstNode* senp = cgp->eventp()->sensesp(); senp; senp = senp->nextp()) { - AstSenItem* const senItemp = VN_AS(senp, SenItem); - if (AstVarRef* const varrefp // LCOV_EXCL_BR_LINE - = VN_CAST(senItemp->sensp(), VarRef)) { - if (varrefp->varp()->isClassMember()) { - cgp->v3warn(COVERIGN, "Unsupported: 'covergroup' clocking event " - "on member variable"); - eventUnsupported = true; + if (hasEmbeddedEventExpr(cgp)) { + // The clocking event references a member of the enclosing class, so it + // names a per-instance signal that V3Active's static-sensitivity path + // cannot express. Under --timing, lower it fully: a forked + // 'forever @(event) sample()' process spawned in the enclosing + // constructor samples on every occurrence of the event regardless of + // where the signal is written (IEEE 1800-2023 19.8.1). Without + // --timing, dynamic per-instance event waits are unavailable, so fall + // back to a best-effort instrumentation of in-class assignments. + if (v3Global.opt.timing().isSetTrue()) { + 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; - break; } } - } - - 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()); + VL_DO_DANGLING(pushDeletep(cgp->unlinkFrBack()), cgp); itemp = nextp; continue; } - // Remove the AstCovergroup node - either unsupported event or no event - VL_DO_DANGLING(pushDeletep(cgp->unlinkFrBack()), cgp); + // Event references only static/global signals. 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; + continue; } itemp = nextp; } @@ -1804,24 +2186,33 @@ class FunctionalCoverageVisitor final : public VNVisitor { iterateChildren(nodep); - // Option B safety net for embedded covergroups: if a coverpoint/iff/cross - // references a member of the enclosing class, lowering would emit uncompilable - // C++ (no handle to the enclosing instance). Skip this covergroup with a clean - // warning rather than crashing the C++ compile. (Full support - an enclosing - // back-pointer - is the planned follow-up.) - if (AstNode* const offenderp = findEnclosingMemberRef(nodep)) { - offenderp->v3warn(COVERIGN, - "Unsupported: 'covergroup' coverpoint referencing enclosing " - "class member; ignoring covergroup " - << nodep->prettyNameQ()); - for (AstCoverpoint* cpp : m_coverpoints) { - VL_DO_DANGLING(pushDeletep(cpp->unlinkFrBack()), cpp); + // Embedded covergroups (IEEE 1800-2023 19.4): coverage constructs may reference + // members of the enclosing class. The covergroup is lowered into a sibling + // class with no implicit handle to the enclosing instance, so install an + // explicit back-pointer and route the references through it. For anything + // outside that form, fall back to the COVERIGN safety net: + // skip the covergroup with a clean warning rather than emitting uncompilable C++. + if (AstNode* const offenderp = findEnclosingMemberRef()) { + if (!installEnclosingBackPointer()) { + offenderp->v3warn(COVERIGN, + "Unsupported: 'covergroup' coverage construct referencing " + "enclosing class member; ignoring covergroup " + << 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(); // 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); } } 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); } } diff --git a/src/V3EmitCHeaders.cpp b/src/V3EmitCHeaders.cpp index 8b2f2add0..138e5adc3 100644 --- a/src/V3EmitCHeaders.cpp +++ b/src/V3EmitCHeaders.cpp @@ -249,8 +249,37 @@ class EmitCHeader final : public EmitCConstInit { if (const AstClass* const classp = VN_CAST(modp, Class)) { if (!classp->isInterfaceClass() && !classp->isVirtual()) { decorateFirst(first, section); - putns(classp, "VlClass* clone() const { return new " - + EmitCUtil::prefixNameProtect(classp) + "(*this); }\n"); + std::vector embeddedCovergroupVars; + std::vector classes; + for (AstClass* scanClassp = const_cast(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"); + } } } } diff --git a/src/V3LinkDot.cpp b/src/V3LinkDot.cpp index 33e2d38b5..879409a67 100644 --- a/src/V3LinkDot.cpp +++ b/src/V3LinkDot.cpp @@ -3437,6 +3437,16 @@ class LinkDotResolveVisitor final : public VNVisitor { } 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) { // Also used for standard 'extends' from a base class UINFO(8, indent() << "importDerivedClass to " << derivedClassp << " from " << baseClassp); @@ -4313,6 +4323,14 @@ class LinkDotResolveVisitor final : public VNVisitor { } else { 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. // Parameters and typedefs are always visible through a modport (IEEE 1800-2023 25.5). // This mirrors the VarXRef modport parameter fallback in visit(AstVarXRef). diff --git a/test_regress/t/coverage_covergroup_common.py b/test_regress/t/coverage_covergroup_common.py index 5bea9d373..7c33beddc 100644 --- a/test_regress/t/coverage_covergroup_common.py +++ b/test_regress/t/coverage_covergroup_common.py @@ -45,8 +45,8 @@ def covergroup_coverage_report(test, outfile=None): return outfile -def run(test, *, verilator_flags2=()): - test.compile(verilator_flags2=['--coverage', *verilator_flags2]) +def run(test, *, verilator_flags2=(), timing_loop=False): + test.compile(verilator_flags2=['--coverage', *verilator_flags2], timing_loop=timing_loop) test.execute() covergroup_coverage_report(test) test.files_identical(test.obj_dir + '/covergroup_report.txt', test.golden_filename) diff --git a/test_regress/t/t_covergroup_embedded.out b/test_regress/t/t_covergroup_embedded.out new file mode 100644 index 000000000..49d258d80 --- /dev/null +++ b/test_regress/t/t_covergroup_embedded.out @@ -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 diff --git a/test_regress/t/t_covergroup_embedded_unsup.py b/test_regress/t/t_covergroup_embedded.py similarity index 85% rename from test_regress/t/t_covergroup_embedded_unsup.py rename to test_regress/t/t_covergroup_embedded.py index 77a0ac64b..91ff59e89 100755 --- a/test_regress/t/t_covergroup_embedded_unsup.py +++ b/test_regress/t/t_covergroup_embedded.py @@ -9,8 +9,8 @@ import vltest_bootstrap +import coverage_covergroup_common + test.scenarios('vlt') -test.lint(expect_filename=test.golden_filename, fails=True) - -test.passes() +coverage_covergroup_common.run(test) diff --git a/test_regress/t/t_covergroup_embedded.v b/test_regress/t/t_covergroup_embedded.v new file mode 100644 index 000000000..27e2c0c51 --- /dev/null +++ b/test_regress/t/t_covergroup_embedded.v @@ -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 diff --git a/test_regress/t/t_covergroup_embedded_timing.out b/test_regress/t/t_covergroup_embedded_timing.out new file mode 100644 index 000000000..70d5bfc0f --- /dev/null +++ b/test_regress/t/t_covergroup_embedded_timing.out @@ -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 diff --git a/test_regress/t/t_covergroup_embedded_timing.py b/test_regress/t/t_covergroup_embedded_timing.py new file mode 100644 index 000000000..4c3b0290b --- /dev/null +++ b/test_regress/t/t_covergroup_embedded_timing.py @@ -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) diff --git a/test_regress/t/t_covergroup_embedded_timing.v b/test_regress/t/t_covergroup_embedded_timing.v new file mode 100644 index 000000000..0c7c8f01c --- /dev/null +++ b/test_regress/t/t_covergroup_embedded_timing.v @@ -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 diff --git a/test_regress/t/t_covergroup_embedded_unsup.out b/test_regress/t/t_covergroup_embedded_unsup.out deleted file mode 100644 index 93cf7a3a5..000000000 --- a/test_regress/t/t_covergroup_embedded_unsup.out +++ /dev/null @@ -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 diff --git a/test_regress/t/t_covergroup_embedded_unsup.v b/test_regress/t/t_covergroup_embedded_unsup.v deleted file mode 100644 index a35ffb840..000000000 --- a/test_regress/t/t_covergroup_embedded_unsup.v +++ /dev/null @@ -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 diff --git a/test_regress/t/t_covergroup_member_event_unsup.out b/test_regress/t/t_covergroup_member_event_unsup.out index e9abfdcd4..38e1831df 100644 --- a/test_regress/t/t_covergroup_member_event_unsup.out +++ b/test_regress/t/t_covergroup_member_event_unsup.out @@ -1,7 +1,14 @@ -%Warning-COVERIGN: t/t_covergroup_member_event_unsup.v:13:5: Unsupported: 'covergroup' clocking event on member variable - : ... note: In instance 't' - 13 | covergroup cov1 @m_z; - | ^~~~~~~~~~ +%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' + 19 | coverpoint m_x {bins lo = {[0 : 7]}; bins hi = {[8 : 15]};} + | ^~~ ... 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. +%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 diff --git a/test_regress/t/t_covergroup_member_event_unsup.py b/test_regress/t/t_covergroup_member_event_unsup.py index 695572bb1..189ca8a23 100755 --- a/test_regress/t/t_covergroup_member_event_unsup.py +++ b/test_regress/t/t_covergroup_member_event_unsup.py @@ -11,6 +11,10 @@ import vltest_bootstrap 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() diff --git a/test_regress/t/t_covergroup_member_event_unsup.v b/test_regress/t/t_covergroup_member_event_unsup.v index d08249b48..32aad8437 100644 --- a/test_regress/t/t_covergroup_member_event_unsup.v +++ b/test_regress/t/t_covergroup_member_event_unsup.v @@ -4,17 +4,65 @@ // SPDX-FileCopyrightText: 2024 Wilson Snyder // SPDX-License-Identifier: CC0-1.0 -module t ( - input clk -); - class Packet; - int m_z; - int m_x; - covergroup cov1 @m_z; - coverpoint m_x; +// Embedded covergroups (IEEE 1800-2023 19.4) whose clocking event or coverage +// constructs reference enclosing-class members in ways Verilator cannot fully +// support. Each must emit a clean COVERIGN warning rather than silently +// producing wrong coverage or uncompilable C++. + +module t; + // Coverpoint references an enclosing member, but the covergroup is never + // 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 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 + nc = new; + ec = new; + cx = new; $write("*-* All Finished *-*\n"); $finish; end diff --git a/test_regress/t/t_covergroup_unsup_ign2.out b/test_regress/t/t_covergroup_unsup_ign2.out index 73065a5c5..a5bfa6bd1 100644 --- a/test_regress/t/t_covergroup_unsup_ign2.out +++ b/test_regress/t/t_covergroup_unsup_ign2.out @@ -300,7 +300,7 @@ : ... note: In instance 't' 177 | cross a, b { | ^ -%Warning-COVERIGN: t/t_covergroup_unsup.v:209:5: Unsupported: 'covergroup' clocking event on member variable - : ... note: In instance 't' - 209 | covergroup cov1 @m_z; - | ^~~~~~~~~~ +%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' + 210 | cp_x: coverpoint m_x; + | ^~~