From 007eac25fd5b6540b24ac8a8996164624f4dd59d Mon Sep 17 00:00:00 2001 From: wsxarcher Date: Thu, 25 Jun 2026 12:48:32 +0200 Subject: [PATCH] Support embedded covergroup member references --- include/verilated_types.h | 36 +- src/V3Covergroup.cpp | 415 ++++++++++++++++-- src/V3EmitCHeaders.cpp | 33 +- src/V3LinkDot.cpp | 18 + test_regress/t/t_covergroup_embedded.out | 30 ++ ...dded_unsup.py => t_covergroup_embedded.py} | 6 +- test_regress/t/t_covergroup_embedded.v | 197 +++++++++ .../t/t_covergroup_embedded_unsup.out | 7 - test_regress/t/t_covergroup_embedded_unsup.v | 45 -- 9 files changed, 675 insertions(+), 112 deletions(-) create mode 100644 test_regress/t/t_covergroup_embedded.out rename test_regress/t/{t_covergroup_embedded_unsup.py => t_covergroup_embedded.py} (85%) create mode 100644 test_regress/t/t_covergroup_embedded.v delete mode 100644 test_regress/t/t_covergroup_embedded_unsup.out delete mode 100644 test_regress/t/t_covergroup_embedded_unsup.v diff --git a/include/verilated_types.h b/include/verilated_types.h index 40847b65e..ca450290b 100644 --- a/include/verilated_types.h +++ b/include/verilated_types.h @@ -2086,15 +2086,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: @@ -2134,58 +2135,73 @@ 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 + 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 diff --git a/src/V3Covergroup.cpp b/src/V3Covergroup.cpp index b1890e001..9ba2d6a15 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,20 @@ class FunctionalCoverageVisitor final : public VNVisitor { , crossBins{cb} {} }; std::vector m_binInfos; // All bins in current covergroup + + struct EmbeddedEventTrigger final { + AstVar* baseVarp; + AstVar* memberVarp; + VEdgeType edgeType; + AstVar* prevVarp; + EmbeddedEventTrigger(AstVar* baseVarp, AstVar* memberVarp, VEdgeType edgeType, + AstVar* prevVarp) + : 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 +1709,195 @@ 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 { + 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* 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"); + FileLine* const fl = itemp->fileline(); + EmbeddedEventTrigger trigger{baseVarp, memberVarp, itemp->edgeType(), nullptr}; + AstVar* const prevVarp = newEventPrevVar(fl, trigger); + m_enclosingClassp->addMembersp(prevVarp); + trigger.prevVarp = prevVarp; + triggers.push_back(trigger); + } + return triggers; + } + + void installEmbeddedEventTriggers(const std::vector& triggers) { + if (triggers.empty()) return; + m_enclosingClassp->foreach([&](AstNodeAssign* asgnp) { + for (const EmbeddedEventTrigger& trigger : triggers) { + if (!isEventLvalue(asgnp->lhsp(), trigger)) continue; + 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 +1905,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 +1916,150 @@ 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;'. + 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("decltype("); + weakThisp->add(lhsp->cloneTree(false)); + weakThisp->add(")::weak(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; // Extract and store the clocking event from AstCovergroup node // The parser creates this node to preserve the event information @@ -1748,33 +2072,26 @@ 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; - 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()); + embeddedEventTriggers = collectEmbeddedEventTriggers(cgp); + if (!embeddedEventTriggers.empty()) { + 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); + if (hasEmbeddedEventExpr(cgp)) { + cgp->v3warn(COVERIGN, "Unsupported: 'covergroup' clocking event " + "on complex member expression"); + hasUnsupportedEvent = true; + VL_DO_DANGLING(pushDeletep(cgp->unlinkFrBack()), cgp); + itemp = nextp; + continue; + } + // 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 +2121,28 @@ 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); + } + return; } - for (AstCoverCross* crossp : m_coverCrosses) { - VL_DO_DANGLING(pushDeletep(crossp->unlinkFrBack()), crossp); - } - return; } + installEmbeddedEventTriggers(embeddedEventTriggers); processCovergroup(); // Remove lowered coverpoints/crosses from the class - they have been @@ -1833,6 +2154,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 2a3197112..9ecb987c6 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 7dd5fe3ea..3cbc3b3f3 100644 --- a/src/V3LinkDot.cpp +++ b/src/V3LinkDot.cpp @@ -3431,6 +3431,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); @@ -4307,6 +4317,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/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_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