// -*- mode: C++; c-file-style: "cc-mode" -*- //************************************************************************* // DESCRIPTION: Verilator: Functional coverage implementation // // Code available from: https://verilator.org // //************************************************************************* // // This program is free software; you can redistribute it and/or modify it // under the terms of either the GNU Lesser General Public License Version 3 // or the Perl Artistic License Version 2.0. // SPDX-FileCopyrightText: 2003-2026 Wilson Snyder // SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 // //************************************************************************* // FUNCTIONAL COVERAGE TRANSFORMATIONS: // For each covergroup (AstClass with isCovergroup()): // For each coverpoint (AstCoverpoint): // Generate member variable for VerilatedCoverpoint // Generate initialization in constructor // Generate sample code in sample() method // //************************************************************************* #include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT #include "V3Covergroup.h" #include "V3Const.h" #include "V3File.h" #include "V3MemberMap.h" #include #include VL_DEFINE_DEBUG_FUNCTIONS; //###################################################################### // Functional coverage visitor class FunctionalCoverageVisitor final : public VNVisitor { // NODE STATE // Entire netlist: // AstCoverpoint::user1p() -> AstVar*. Previous-value variable for transition bins const VNUser1InUse m_inuser1; // STATE AstClass* m_covergroupp = nullptr; // Current covergroup being processed AstClass* m_enclosingClassp = nullptr; // Class lexically enclosing the covergroup, if any AstVar* m_embeddedVarp = nullptr; // Embedded covergroup member of m_enclosingClassp, if any AstFunc* m_sampleFuncp = nullptr; // Current sample() function AstFunc* m_constructorp = nullptr; // Current constructor std::vector m_coverpoints; // Coverpoints in current covergroup std::map m_coverpointMap; // Name -> coverpoint for fast lookup std::vector m_coverCrosses; // Cross coverage items in current covergroup struct EmbeddedEventTrigger final { FileLine* eventFl; // Clocking-event source location AstVar* baseVarp; // Base enclosing-class member in the event expression AstVar* memberVarp; // Selected member in a 'base.member' expression, or nullptr VEdgeType edgeType; // Clocking-event edge qualifier AstVar* prevVarp; // Member containing the previous event value, or nullptr EmbeddedEventTrigger(FileLine* eventFl, AstVar* baseVarp, AstVar* memberVarp, VEdgeType edgeType) : eventFl{eventFl} , baseVarp{baseVarp} , memberVarp{memberVarp} , edgeType{edgeType} , prevVarp{nullptr} {} }; std::set m_crossedCpNames; // Coverpoints referenced by a cross std::vector m_cpVars; // VlCoverpoint member, one per coverpoint std::vector m_crossVars; // VlCoverCross member, one per cross std::map m_cpVarMap; // Coverpoint name -> its VlCoverpoint member std::set m_droppedCrosses; // Crosses with a bare-variable item: drop (COVERIGN) std::map m_vlCoverpointTypes; // hit-list bound K -> "VlCoverpointT" type AstCDType* m_vlCoverCrossDTypep = nullptr; // Shared "VlCoverCross" C++ member type VMemberMap m_memberMap; // Member names cached for fast lookup // METHODS void processCovergroup() { UINFO(4, "Processing covergroup: " << m_covergroupp->name() << " with " << m_coverpoints.size() << " coverpoints and " << m_coverCrosses.size() << " crosses"); m_crossedCpNames.clear(); m_cpVars.clear(); m_crossVars.clear(); m_cpVarMap.clear(); m_droppedCrosses.clear(); // Scan every cross item to record the coverpoints it references (the cross dimensions) // and to flag any cross naming a bare variable -- a would-be implicit coverpoint, which // Verilator does not synthesize. An unresolvable item drops only that one cross (with a // COVERIGN in generateCrossCode), leaving the rest of the covergroup intact. for (AstCoverCross* crossp : m_coverCrosses) { for (AstNode* itemp = crossp->itemsp(); itemp; itemp = itemp->nextp()) { const AstCoverpointRef* const refp = VN_AS(itemp, CoverpointRef); if (refp->exprp()) continue; // hierarchical ref: dropped in generateCrossCode if (m_coverpointMap.find(refp->name()) == m_coverpointMap.end()) { m_droppedCrosses.insert(crossp); // bare variable: drop this cross only } else { m_crossedCpNames.insert(refp->name()); } } } // For each coverpoint, generate sampling code for (AstCoverpoint* cpp : m_coverpoints) generateCoverpointCode(cpp); // For each cross, generate sampling code for (AstCoverCross* crossp : m_coverCrosses) generateCrossCode(crossp); // Generate coverage computation code (even for empty covergroups). Bin registration // with the coverage database is handled per coverpoint/cross by their runtime // registerBins() calls (emitted in generateCoverpoint/generateCross). // TODO: Generate instance registry infrastructure for static get_coverage() // This requires: // - Static registry members (t_instances, s_mutex) // - registerInstance() / unregisterInstance() methods // - Proper C++ emission in EmitC backend // For now, get_coverage() returns 0.0 (placeholder) generateCoverageComputationCode(); } static constexpr int COVER_BINS_LIMIT = 1000; // Sanity limit to avoid hangs from e.g. signed underflow void expandAutomaticBins(AstCoverpoint* coverpointp, AstNodeExpr* exprp) { // Find and expand any automatic bins AstNode* prevBinp = nullptr; for (AstNode* binp = coverpointp->binsp(); binp;) { AstCoverBin* const cbinp = VN_AS(binp, CoverBin); AstNode* const nextBinp = binp->nextp(); if (cbinp->binsType() == VCoverBinsType::BINS_AUTO) { UINFO(4, " Expanding automatic bin: " << cbinp->name()); // Get array size - must be a constant AstNodeExpr* const sizep = cbinp->arraySizep(); // Evaluate as constant const AstConst* constp = VN_CAST(sizep, Const); if (!constp) { cbinp->v3error("Automatic bins array size must be a constant"); binp = nextBinp; continue; } const int numBins = constp->toSInt(); if (numBins <= 0) { cbinp->v3error("Automatic bins array size must be >= 1, got " << numBins); binp = nextBinp; continue; } if (numBins > COVER_BINS_LIMIT) { cbinp->v3error("Automatic bins array size of " << numBins << " exceeds limit of " << COVER_BINS_LIMIT); binp = nextBinp; continue; } // Calculate range division const int width = exprp->width(); const uint64_t maxVal = (width >= 64) ? UINT64_MAX : ((1ULL << width) - 1); // For width >= 64: (maxVal+1) would overflow; compute binSize without overflow const uint64_t binSize = (width < 64) ? ((maxVal + 1) / numBins) : (UINT64_MAX / numBins + 1); UINFO(4, " Width=" << width << " maxVal=" << maxVal << " numBins=" << numBins << " binSize=" << binSize); // Create expanded bins for (int i = 0; i < numBins; i++) { const uint64_t lo = static_cast(i) * binSize; const uint64_t hi = (i == numBins - 1) ? maxVal : ((i + 1) * binSize - 1); // Create constants for range (use setQuad to handle values > 32-bit) V3Number loNum{cbinp->fileline(), width, 0}; loNum.setQuad(lo); AstConst* const loConstp = new AstConst{cbinp->fileline(), loNum}; V3Number hiNum{cbinp->fileline(), width, 0}; hiNum.setQuad(hi); AstConst* const hiConstp = new AstConst{cbinp->fileline(), hiNum}; // Create InsideRange [lo:hi] AstInsideRange* const rangep = new AstInsideRange{cbinp->fileline(), loConstp, hiConstp}; rangep->dtypeFrom(exprp); // Set dtype from coverpoint expression // Create new bin const string binName = cbinp->name() + "[" + std::to_string(i) + "]"; AstCoverBin* const newBinp = new AstCoverBin{cbinp->fileline(), binName, rangep, false, false}; // Insert after previous bin if (prevBinp) { prevBinp->addNext(newBinp); } else { coverpointp->addBinsp(newBinp); } prevBinp = newBinp; } // Remove the AUTO bin from the list VL_DO_DANGLING(pushDeletep(binp->unlinkFrBack()), binp); } else { prevBinp = binp; } binp = nextBinp; } } // Extract all coverpoint option values in a single pass. // atLeastOut: option.at_least (default 1) // autoBinMaxOut: option.auto_bin_max (coverpoint overrides covergroup, default 64) void extractCoverpointOptions(AstCoverpoint* coverpointp, int& atLeastOut, int& autoBinMaxOut) { atLeastOut = 1; autoBinMaxOut = -1; // -1 = not set at coverpoint level for (AstNode* optionp = coverpointp->optionsp(); optionp; optionp = optionp->nextp()) { AstCoverOption* const optp = VN_AS(optionp, CoverOption); AstConst* const constp = VN_CAST(optp->valuep(), Const); if (!constp) { optp->valuep()->v3warn(COVERIGN, "Ignoring unsupported: non-constant 'option." << optp->optionType().ascii() << "'; using default value"); continue; } if (optp->optionType() == VCoverOptionType::AT_LEAST) { atLeastOut = constp->toSInt(); } else { // V3LinkParse only converts at_least/auto_bin_max coverpoint options into // AstCoverOption (others are dropped there), so this is the only alternative. UASSERT_OBJ(optp->optionType() == VCoverOptionType::AUTO_BIN_MAX, optp, "Unexpected coverpoint option type reaching V3Covergroup"); autoBinMaxOut = constp->toSInt(); } } // Fall back to covergroup-level auto_bin_max if not set at coverpoint level if (autoBinMaxOut < 0) { if (m_covergroupp->cgAutoBinMax() >= 0) { autoBinMaxOut = m_covergroupp->cgAutoBinMax(); } else { autoBinMaxOut = 64; // Default per IEEE 1800-2023 Table 19-1 } } } // Extract individual values from a range expression list, used only to carve values // out of implicit auto-bins. Iterates over all siblings (nextp) in the list, handling // AstConst (single value) and AstInsideRange ([lo:hi]); an open-ended bound ('$', // AstUnbounded) resolves to the coverpoint domain min (lower) or max (upper, == maxVal). void extractValuesFromRange(AstNode* nodep, std::set& values, uint64_t maxVal) { // Cap enumeration so a '$'-bounded or otherwise huge range cannot blow up memory; // auto-bins are per-value only for small domains, so a partial set is harmless here. constexpr size_t maxEnumerate = 1ULL << 16; for (AstNode* np = nodep; np; np = np->nextp()) { if (AstConst* constp = VN_CAST(np, Const)) { if (constp->num().isFourState()) continue; // wildcard patterns can't be enumerated values.insert(constp->toUQuad()); } else if (AstInsideRange* rangep = VN_CAST(np, InsideRange)) { AstNodeExpr* const lhsp = V3Const::constifyEdit(rangep->lhsp()); AstNodeExpr* const rhsp = V3Const::constifyEdit(rangep->rhsp()); const bool loUnbounded = VN_IS(lhsp, Unbounded); const bool hiUnbounded = VN_IS(rhsp, Unbounded); AstConst* const loConstp = VN_CAST(lhsp, Const); AstConst* const hiConstp = VN_CAST(rhsp, Const); if ((!loConstp && !loUnbounded) || (!hiConstp && !hiUnbounded)) { rangep->v3error("Non-constant expression in bin range; " "range bounds must be constants"); continue; } if ((loConstp && loConstp->num().isFourState()) || (hiConstp && hiConstp->num().isFourState())) continue; const uint64_t lo = loUnbounded ? 0 : loConstp->toUQuad(); const uint64_t hi = hiUnbounded ? maxVal : hiConstp->toUQuad(); for (uint64_t v = lo; v <= hi; v++) { if (values.size() >= maxEnumerate) break; values.insert(v); } } else { np->v3error("Non-constant expression in bin value list; values must be constants"); } } } // Single-pass categorization: determine whether any regular (non-ignore/illegal) bins exist // and collect the set of excluded values from ignore/illegal bins. void categorizeBins(AstCoverpoint* coverpointp, bool& hasRegularOut, std::set& excludedOut, uint64_t maxVal) { hasRegularOut = false; for (AstNode* binp = coverpointp->binsp(); binp; binp = binp->nextp()) { AstCoverBin* const cbinp = VN_AS(binp, CoverBin); const VCoverBinsType btype = cbinp->binsType(); if (btype == VCoverBinsType::BINS_IGNORE || btype == VCoverBinsType::BINS_ILLEGAL) { if (AstNode* rangep = cbinp->rangesp()) { extractValuesFromRange(rangep, excludedOut, maxVal); } } else { hasRegularOut = true; } } } // Create implicit automatic bins when coverpoint has no explicit regular bins void createImplicitAutoBins(AstCoverpoint* coverpointp, AstNodeExpr* exprp, int autoBinMax) { const int width = exprp->width(); const uint64_t maxVal = (width >= 64) ? UINT64_MAX : ((1ULL << width) - 1); // Single pass: check for regular bins and collect excluded values simultaneously. // maxVal resolves any '$' (open-ended) bound in ignore_bins/illegal_bins ranges. bool hasRegular = false; std::set excluded; categorizeBins(coverpointp, hasRegular, excluded, maxVal); // If already has regular bins, nothing to do if (hasRegular) return; UINFO(4, " Creating implicit automatic bins for coverpoint: " << coverpointp->name()); const uint64_t numTotalValues = (width >= 64) ? UINT64_MAX : (1ULL << width); const uint64_t numValidValues = numTotalValues - excluded.size(); // Determine number of bins to create (based on non-excluded values) int numBins; if (numValidValues <= static_cast(autoBinMax)) { // Create one bin per valid value numBins = numValidValues; } else { // Create autoBinMax bins, dividing range numBins = autoBinMax; } UINFO(4, " Width=" << width << " numTotalValues=" << numTotalValues << " numValidValues=" << numValidValues << " autoBinMax=" << autoBinMax << " creating " << numBins << " bins"); // Strategy: Create bins for each value (if numValidValues <= autoBinMax) // or create range bins that avoid excluded values if (numValidValues <= static_cast(autoBinMax)) { // Create one bin per valid value int binCount = 0; for (uint64_t v = 0; v <= maxVal && binCount < numBins; v++) { // Skip excluded values if (excluded.find(v) != excluded.end()) continue; // Create single-value bin AstConst* const valConstp = new AstConst{ coverpointp->fileline(), V3Number(coverpointp->fileline(), width, v)}; AstConst* const valConstp2 = new AstConst{ coverpointp->fileline(), V3Number(coverpointp->fileline(), width, v)}; AstInsideRange* const rangep = new AstInsideRange{coverpointp->fileline(), valConstp, valConstp2}; rangep->dtypeFrom(exprp); const string binName = "auto_" + std::to_string(binCount); AstCoverBin* const newBinp = new AstCoverBin{coverpointp->fileline(), binName, rangep, false, false}; coverpointp->addBinsp(newBinp); binCount++; } UINFO(4, " Created " << binCount << " single-value automatic bins"); } else { // Create range bins (more complex - need to handle excluded values in ranges) // For simplicity, create bins and let excluded values not match any bin const uint64_t binSize = (maxVal + 1) / numBins; for (int i = 0; i < numBins; i++) { const uint64_t lo = i * binSize; const uint64_t hi = (i == numBins - 1) ? maxVal : ((i + 1) * binSize - 1); // Create constants for range AstConst* const loConstp = new AstConst{ coverpointp->fileline(), V3Number(coverpointp->fileline(), width, lo)}; AstConst* const hiConstp = new AstConst{ coverpointp->fileline(), V3Number(coverpointp->fileline(), width, hi)}; // Create InsideRange [lo:hi] AstInsideRange* const rangep = new AstInsideRange{coverpointp->fileline(), loConstp, hiConstp}; rangep->dtypeFrom(exprp); // Create bin name const string binName = "auto_" + std::to_string(i); AstCoverBin* const newBinp = new AstCoverBin{coverpointp->fileline(), binName, rangep, false, false}; // Add to coverpoint coverpointp->addBinsp(newBinp); } UINFO(4, " Created range-based automatic bins"); } } // Sanitize generated names to be valid C++ identifiers static string sanitizeGeneratedName(string name) { std::replace(name.begin(), name.end(), '[', '_'); std::replace(name.begin(), name.end(), ']', '_'); return name; } // Capture an iff guard in a function-local temporary so it is evaluated once per sample() AstVarRef* captureIffToTemp(AstNodeExpr* iffp, const string& tempName) { FileLine* const fl = iffp->fileline(); AstVar* const iffVarp = new AstVar{fl, VVarType::BLOCKTEMP, tempName, iffp->findBitDType()}; iffVarp->funcLocal(true); m_sampleFuncp->addStmtsp(iffVarp); iffp->unlinkFrBack(); m_sampleFuncp->addStmtsp( new AstAssign{fl, new AstVarRef{fl, iffVarp, VAccess::WRITE}, iffp}); return new AstVarRef{fl, iffVarp, VAccess::READ}; } AstNodeExpr* applyCoverpointIffCondition(AstCoverpoint* coverpointp, FileLine* fl, AstNodeExpr* condp) { if (AstNodeExpr* const iffp = coverpointp->iffp()) { UINFO(6, " Adding iff condition"); condp = new AstAnd{fl, iffp->cloneTree(false), condp}; } return condp; } // Create previous value variable for transition tracking AstVar* createPrevValueVar(AstCoverpoint* coverpointp, AstNodeExpr* exprp) { // Check if already created if (AstVar* const prevVarp = VN_CAST(coverpointp->user1p(), Var)) return prevVarp; // Create variable to store previous sampled value const string varName = "__Vprev_" + coverpointp->name(); AstVar* prevVarp = new AstVar{coverpointp->fileline(), VVarType::MEMBER, varName, exprp->dtypep()}; prevVarp->isStatic(false); m_covergroupp->addMembersp(prevVarp); UINFO(4, " Created previous value variable: " << varName); // Initialize to zero in constructor AstNodeExpr* const initExprp = new AstConst{prevVarp->fileline(), AstConst::WidthedValue{}, prevVarp->width(), 0}; AstNodeStmt* const initStmtp = new AstAssign{ prevVarp->fileline(), new AstVarRef{prevVarp->fileline(), prevVarp, VAccess::WRITE}, initExprp}; m_constructorp->addStmtsp(initStmtp); coverpointp->user1p(prevVarp); return prevVarp; } // Create state position variable for multi-value transition bins // Tracks position in sequence: 0=not started, 1=seen first item, etc. AstVar* createSequenceStateVar(AstCoverpoint* coverpointp, AstCoverBin* binp) { // Create variable to track sequence position const string varName = "__Vseqpos_" + coverpointp->name() + "_" + binp->name(); // Use 8-bit integer for state position (sequences rarely > 255 items) AstVar* stateVarp = new AstVar{binp->fileline(), VVarType::MEMBER, varName, VFlagLogicPacked{}, 8}; stateVarp->isStatic(false); m_covergroupp->addMembersp(stateVarp); UINFO(4, " Created sequence state variable: " << varName); // Initialize to 0 (not started) in constructor AstNodeStmt* const initStmtp = new AstAssign{ stateVarp->fileline(), new AstVarRef{stateVarp->fileline(), stateVarp, VAccess::WRITE}, new AstConst{stateVarp->fileline(), AstConst::WidthedValue{}, 8, 0}}; m_constructorp->addStmtsp(initStmtp); return stateVarp; } void generateCoverpointCode(AstCoverpoint* coverpointp) { UINFO(4, " Generating code for coverpoint: " << coverpointp->name()); // Get the coverpoint expression AstNodeExpr* const exprp = coverpointp->exprp(); // Expand automatic bins before processing expandAutomaticBins(coverpointp, exprp); // Extract all coverpoint options in a single pass int atLeastValue; int autoBinMax; extractCoverpointOptions(coverpointp, atLeastValue, autoBinMax); UINFO(6, " Coverpoint at_least = " << atLeastValue << " auto_bin_max = " << autoBinMax); // Create implicit automatic bins if no regular bins exist createImplicitAutoBins(coverpointp, exprp, autoBinMax); // Every coverpoint routes through the VlCoverpoint runtime. Transition coverpoints are // included: their per-value matching is still generated as a state machine in sample() // (see generateCoverpoint), but the bin hit is recorded in the runtime bin // rather than a bare counter. generateCoverpoint(coverpointp, exprp, atLeastValue); } // Build the condition under which a default bin matches: NOT(OR of all normal bins). AstNodeExpr* buildDefaultCondition(AstCoverpoint* coverpointp, AstNodeExpr* exprp, FileLine* fl) { AstNodeExpr* anyBinMatchp = nullptr; for (AstNode* binp = coverpointp->binsp(); binp; binp = binp->nextp()) { AstCoverBin* const cbinp = VN_AS(binp, CoverBin); if (cbinp->binsType() == VCoverBinsType::BINS_DEFAULT || cbinp->binsType() == VCoverBinsType::BINS_IGNORE || cbinp->binsType() == VCoverBinsType::BINS_ILLEGAL) continue; AstNodeExpr* const binCondp = buildBinCondition(cbinp, exprp); UASSERT_OBJ(binCondp, cbinp, "buildBinCondition returned nullptr for non-ignore/non-illegal bin"); anyBinMatchp = anyBinMatchp ? new AstOr{fl, anyBinMatchp, binCondp} : binCondp; } return anyBinMatchp ? static_cast(new AstNot{fl, anyBinMatchp}) : static_cast(new AstConst{fl, AstConst::BitTrue{}}); } //==================================================================== // VlCoverpoint conversion // True if a coverpoint has any transition bin. Used to decide whether sample() emits the // end-of-sample previous-value update that transition matching needs. static bool coverpointHasTransition(AstCoverpoint* coverpointp) { for (AstNode* binp = coverpointp->binsp(); binp; binp = binp->nextp()) { if (VN_AS(binp, CoverBin)->transp()) return true; } return false; } // Get (or create) the "VlCoverpointT" member type for hit-list bound K. AstCDType* vlCoverpointType(FileLine* fl, int hitBound) { const auto it = m_vlCoverpointTypes.find(hitBound); if (it != m_vlCoverpointTypes.end()) return it->second; AstCDType* const typep = new AstCDType{fl, "VlCoverpointT<" + std::to_string(hitBound) + ">"}; v3Global.rootp()->typeTablep()->addTypesp(typep); m_vlCoverpointTypes.emplace(hitBound, typep); return typep; } // Constant bounds of one rangesp() element (an InsideRange or a single Const). Each bound is // the raw AST node -- an AstConst or an AstUnbounded ('$') -- with the const/unbounded view // derived on demand, so there is one source of truth per bound. After a successful // constRangeBounds() neither node is null; a single Const has both bounds aliasing one node. struct RangeBounds final { AstNode* loNodep = nullptr; // low bound: AstConst or AstUnbounded AstNode* hiNodep = nullptr; // high bound: AstConst or AstUnbounded bool loUnbounded() const { return VN_IS(loNodep, Unbounded); } bool hiUnbounded() const { return VN_IS(hiNodep, Unbounded); } AstConst* loConstp() const { return VN_CAST(loNodep, Const); } AstConst* hiConstp() const { return VN_CAST(hiNodep, Const); } }; // Decode one rangesp() element into its constant bounds. Returns false if rp is neither an // InsideRange nor a single Const, or if a present bound is non-constant or 4-state. '$' // bounds are left as AstUnbounded (not resolved) -- the caller applies its own policy. // Centralizes the InsideRange/Const/Unbounded decode shared by the hit-list-bound paths. static bool constRangeBounds(AstNode* rp, RangeBounds& rb) { if (AstInsideRange* const irp = VN_CAST(rp, InsideRange)) { rb.loNodep = irp->lhsp(); rb.hiNodep = irp->rhsp(); } else if (AstConst* const cp = VN_CAST(rp, Const)) { rb.loNodep = rb.hiNodep = cp; } else { return false; } // Each bound must be a constant unless it is '$'; reject non-const and 4-state. AstConst* const lc = rb.loConstp(); AstConst* const hc = rb.hiConstp(); if ((!lc && !rb.loUnbounded()) || (!hc && !rb.hiUnbounded())) return false; if ((lc && lc->num().isFourState()) || (hc && hc->num().isFourState())) return false; return true; } // Collect the covered value intervals of a single (non-array) Normal bin. Returns false // if any range isn't a constant/open InsideRange or single Const (e.g. wildcard, non-const). static bool extractRangeIntervals(AstCoverBin* cbinp, uint64_t maxVal, std::vector>& out) { if (!cbinp->rangesp()) return false; for (AstNode* rp = cbinp->rangesp(); rp; rp = rp->nextp()) { RangeBounds rb; if (!constRangeBounds(rp, rb)) return false; const uint64_t lo = rb.loUnbounded() ? 0 : rb.loConstp()->toUQuad(); const uint64_t hi = rb.hiUnbounded() ? maxVal : rb.hiConstp()->toUQuad(); if (lo > hi) return false; out.emplace_back(lo, hi); } return true; } // Append one Normal bin's cross-slot interval-sets to `bins` and bump `slotCount` by the // number of cross slots the bin contributes. Returns false if any part isn't statically // enumerable (the caller then falls back to the always-safe Normal-slot count). A non-array // bin is one slot covering the union of its intervals; an array bin contributes one // single-value slot per element value (mirroring how it lowers to b[0]..b[N-1]). bool appendBinCrossSlots(AstCoverBin* cbinp, uint64_t maxVal, std::vector>>& bins, int& slotCount) { if (cbinp->isArray()) return appendArrayBinCrossSlots(cbinp, bins, slotCount); // Non-array bin: one slot covering the union of its intervals. ++slotCount; std::vector> ivs; if (cbinp->isWildcard() || !extractRangeIntervals(cbinp, maxVal, ivs)) return false; bins.push_back(std::move(ivs)); return true; } // Append the cross slots of an array Normal bin: each element value is its own single-value // Normal bin. '$'-bounded or non-constant elements can't be enumerated, so they count one // slot but lose exactness. Returns false if any element wasn't enumerable to exact values. bool appendArrayBinCrossSlots(AstCoverBin* cbinp, std::vector>>& bins, int& slotCount) { bool exact = true; for (AstNode* rp = cbinp->rangesp(); rp; rp = rp->nextp()) { RangeBounds rb; if (!constRangeBounds(rp, rb) || rb.loUnbounded() || rb.hiUnbounded()) { ++slotCount; exact = false; } else if (rb.loNodep == rb.hiNodep) { // single Const element (both alias one node) ++slotCount; bins.push_back({{rb.loConstp()->toUQuad(), rb.loConstp()->toUQuad()}}); } else { // [lo:hi] range: one single-value slot per enumerated value for (int64_t v = rb.loConstp()->toSInt(); v <= rb.hiConstp()->toSInt(); ++v) { ++slotCount; bins.push_back({{static_cast(v), static_cast(v)}}); } } } return exact; } // Compute the hit-list bound for a coverpoint: the maximum number of Normal // bins one sample value can match. Non-cross-fed coverpoints don't feed a cross, so // their hit list is unused -> 1. Otherwise compute the exact max bin overlap; fall back // to the (always-safe) Normal-slot count when any bin isn't statically analyzable. int computeHitListBound(AstCoverpoint* coverpointp, AstNodeExpr* exprp, bool crossFed) { if (!crossFed) return 1; const int width = exprp->width(); const uint64_t maxVal = (width >= 64) ? UINT64_MAX : ((1ULL << width) - 1); // One entry per Normal bin (cross slot): its covered intervals. std::vector>> bins; int slotCount = 0; // == runtime m_normal; the safe fallback bound bool exact = true; for (AstNode* binp = coverpointp->binsp(); binp; binp = binp->nextp()) { AstCoverBin* const cbinp = VN_AS(binp, CoverBin); if (!cbinp->binsType().binIsNormal()) continue; // ignore/illegal/default: not hit-listed if (!appendBinCrossSlots(cbinp, maxVal, bins, slotCount)) exact = false; } if (!exact) return std::max(1, slotCount); if (bins.empty()) return 1; // Max overlap occurs at some interval start; count covering bins at each lo. std::vector pts; for (const auto& b : bins) for (const auto& iv : b) pts.push_back(iv.first); std::sort(pts.begin(), pts.end()); pts.erase(std::unique(pts.begin(), pts.end()), pts.end()); int maxOverlap = 1; for (const uint64_t p : pts) { int cnt = 0; for (const auto& b : bins) { for (const auto& iv : b) { if (iv.first <= p && p <= iv.second) { ++cnt; break; // count each bin at most once } } } if (cnt > maxOverlap) maxOverlap = cnt; } return maxOverlap; } // A 'this->m_member' reference for embedding in an AstCStmt AstVarRef* memberRef(FileLine* fl, AstVar* varp) { AstVarRef* const refp = new AstVarRef{fl, varp, VAccess::READ}; refp->selfPointer(VSelfPointerText{VSelfPointerText::This{}}); return refp; } // Individual equality targets of an array bin (bins b[] = {values/ranges}), in order. // An open-ended bound ('$', AstUnbounded) resolves to the coverpoint domain: '[lo:$]' // covers [lo:maxVal] and '[$:hi]' covers [0:hi]. One target is produced per value; a // range whose resolved size would exceed COVER_BINS_LIMIT (e.g. an open '[lo:$]' over a // wide coverpoint) is unsupported -- emits COVERIGN, sets unsupportedOut, yields nothing. std::vector extractArrayValues(AstCoverBin* arrayBinp, AstNodeExpr* exprp, bool& unsupportedOut) { unsupportedOut = false; const int width = exprp->width(); const uint64_t maxVal = (width >= 64) ? UINT64_MAX : ((1ULL << width) - 1); std::vector values; for (AstNode* rangep = arrayBinp->rangesp(); rangep; rangep = rangep->nextp()) { if (AstInsideRange* const irp = VN_CAST(rangep, InsideRange)) { AstNodeExpr* const lhsp = V3Const::constifyEdit(irp->lhsp()); AstNodeExpr* const rhsp = V3Const::constifyEdit(irp->rhsp()); const bool loUnb = VN_IS(lhsp, Unbounded); const bool hiUnb = VN_IS(rhsp, Unbounded); AstConst* const minp = VN_CAST(lhsp, Const); AstConst* const maxp = VN_CAST(rhsp, Const); if ((!minp && !loUnb) || (!maxp && !hiUnb)) { arrayBinp->v3error("Non-constant expression in array bins range; " "range bounds must be constants"); return values; } if ((minp && minp->num().isFourState()) || (maxp && maxp->num().isFourState())) { arrayBinp->v3error("Four-state (x/z) value in array bins range bound; " "range bounds must be two-state constants"); return values; } const uint64_t lo = loUnb ? 0 : minp->toUQuad(); const uint64_t hi = hiUnb ? maxVal : maxp->toUQuad(); if (hi < lo) continue; // empty range contributes no bins // Guard against a '$'-bounded or otherwise huge range exploding the bin count. const uint64_t span = hi - lo; // == valueCount - 1 (no overflow: hi >= lo) if (span >= static_cast(COVER_BINS_LIMIT) || values.size() + span + 1 > static_cast(COVER_BINS_LIMIT)) { arrayBinp->v3warn(COVERIGN, "Unsupported: array 'bins' covering more than " << COVER_BINS_LIMIT << " values (e.g. an open '[lo:$]' range over " "a wide coverpoint); bin ignored"); unsupportedOut = true; for (AstNodeExpr* const vp : values) VL_DO_DANGLING(pushDeletep(vp), vp); values.clear(); return values; } for (uint64_t v = lo; v <= hi; ++v) values.push_back(new AstConst{irp->fileline(), AstConst::WidthedValue{}, width, static_cast(v)}); } else if (VN_IS(rangep, Const)) { values.push_back(VN_AS(rangep->cloneTree(false), NodeExpr)); } else { arrayBinp->v3error("Non-constant expression in array bins value list; " "values must be constants"); return values; } } return values; } // Emit a 'this->m_cp.addSingleNamer/addArrayNamer(...)' statement for one bin AstCStmt* makeNamer(AstVar* cpVarp, AstCoverBin* binp, int count) { FileLine* const fl = binp->fileline(); AstCStmt* const cs = new AstCStmt{fl}; cs->add(memberRef(fl, cpVarp)); // Under --protect-ids the filename and bin name flow into the coverage database // verbatim, so obfuscate them exactly as line/toggle coverage points are (whole- // unit filename, per-word bin name). A no-op when --protect-ids is off. const bool prot = v3Global.opt.protectIds(); const std::string loc = "\"" + VIdProtect::protectIf(fl->filename(), prot) + "\", " + std::to_string(fl->lineno()) + ", " + std::to_string(fl->firstColumn()) + ");"; const std::string binName = VIdProtect::protectWordsIf(binp->name(), prot); if (count < 0) { // single bin cs->add(".addSingleNamer(" + std::string{binp->binsType().binSetEnum()} + ", \"" + binName + "\", " + loc); } else { // value array bin cs->add(".addArrayNamer(" + std::string{binp->binsType().binSetEnum()} + ", " + std::to_string(count) + ", \"" + binName + "\", " + loc); } return cs; } // Emit 'if (iff && cond) m_cp.incrementBin(idx);' (or recordHit, + illegal action) in sample() // Where a bin's hit is recorded in the runtime VlCoverpoint member. struct ConvBinTarget final { AstVar* cpVarp; // the __Vcp_ member int idx; // bin index within that coverpoint bool isNormal; // Normal -> incrementBin (count + cross hit list); else recordHit (count) }; // Emit 'this->m_cp.incrementBin(idx);' (Normal) or '.recordHit(idx);' // (ignore/illegal/default). AstNodeStmt* makeRuntimeBinHit(FileLine* fl, const ConvBinTarget& tgt) { AstCStmt* const cs = new AstCStmt{fl}; cs->add(memberRef(fl, tgt.cpVarp)); cs->add((tgt.isNormal ? ".incrementBin(" : ".recordHit(") + std::to_string(tgt.idx) + ");"); return cs; } void emitConvHitIf(AstCoverpoint* coverpointp, AstCoverBin* binp, AstVar* cpVarp, int idx, AstNodeExpr* condp) { FileLine* const fl = binp->fileline(); AstNode* actionp = makeRuntimeBinHit(fl, {cpVarp, idx, binp->binsType().binIsNormal()}); if (binp->binsType() == VCoverBinsType::BINS_ILLEGAL) { actionp->addNext(makeIllegalBinAction(fl, "Illegal bin " + binp->prettyNameQ() + " hit in coverpoint " + coverpointp->prettyNameQ())); } AstNodeExpr* const guardedp = applyCoverpointIffCondition(coverpointp, fl, condp); UASSERT_OBJ(m_sampleFuncp, binp, "sample() CFunc not set for coverpoint"); m_sampleFuncp->addStmtsp(new AstIf{fl, guardedp, actionp, nullptr}); } // Emit a transition bin's hit action into sample(): // if (iff && cond) { m_cp.incrementBin/recordHit(idx); [illegal: $error; $stop] } // Used by the transition generators so a completed sequence records into the runtime bin. void addConvTransHitIf(AstCoverpoint* coverpointp, AstCoverBin* binp, const ConvBinTarget& tgt, AstNodeExpr* condp) { FileLine* const fl = binp->fileline(); AstNode* actionp = makeRuntimeBinHit(fl, tgt); if (binp->binsType() == VCoverBinsType::BINS_ILLEGAL) { actionp->addNext(makeIllegalBinAction( fl, "Illegal transition bin " + binp->prettyNameQ() + " hit in coverpoint " + coverpointp->prettyNameQ())); } AstNodeExpr* const guardedp = applyCoverpointIffCondition(coverpointp, fl, condp); UASSERT_OBJ(m_sampleFuncp, binp, "sample() CFunc not set for transition bin"); m_sampleFuncp->addStmtsp(new AstIf{fl, guardedp, actionp, nullptr}); } // Route a coverpoint through a VlCoverpoint member: emit the member, its sample() // increments, the constructor configuration (init + namers), and registration. void generateCoverpoint(AstCoverpoint* coverpointp, AstNodeExpr* exprp, int atLeastValue) { FileLine* const fl = coverpointp->fileline(); UINFO(4, " Generating VlCoverpoint member: " << coverpointp->name()); if (AstNodeExpr* const iffp = coverpointp->iffp()) { coverpointp->iffp( captureIffToTemp(iffp, "__VcpIff_" + sanitizeGeneratedName(coverpointp->name()))); } // Size the hit list to the gen-time max bin overlap (1 unless cross-fed with // overlapping ranges), so no cross hit is ever dropped and storage is minimal. const bool crossFed = m_crossedCpNames.count(coverpointp->name()) != 0; const int hitBound = computeHitListBound(coverpointp, exprp, crossFed); UINFO(6, " Hit-list bound (max bin overlap) = " << hitBound); AstVar* const cpVarp = new AstVar{fl, VVarType::MEMBER, "__Vcp_" + coverpointp->name(), vlCoverpointType(fl, hitBound)}; cpVarp->isStatic(false); m_covergroupp->addMembersp(cpVarp); m_cpVars.push_back(cpVarp); m_cpVarMap[coverpointp->name()] = cpVarp; // A cross reads this coverpoint's hit list, so clear it at the start of the // coverpoint's sample() contribution (before any incrementBin appends to it). if (crossFed) { AstCStmt* const clrp = new AstCStmt{fl}; clrp->add(memberRef(fl, cpVarp)); clrp->add(".clearHitList();"); UASSERT_OBJ(m_sampleFuncp, coverpointp, "sample() CFunc not set for clearHitList"); m_sampleFuncp->addStmtsp(clrp); } // Walk bins (non-default, then default), assigning sequential indices that match the // namer append order; emit sample increments and collect namer statements. std::vector namerStmts; std::vector defaultBins; int idx = 0; for (AstNode* binp = coverpointp->binsp(); binp; binp = binp->nextp()) { AstCoverBin* const cbinp = VN_AS(binp, CoverBin); if (cbinp->binsType() == VCoverBinsType::BINS_DEFAULT) { defaultBins.push_back(cbinp); continue; } if (cbinp->transp()) { // Transition bin (incl. array transition 'bins t[] = (a=>b),(c=>d)' and // illegal_bins/ignore_bins transitions). All sequences of one transition bin // share a bin name and merge in the coverage DB to a single point, so model // them as one runtime bin incremented by any matching sequence. The sequence // matching is generated as a state machine, with the hit routed to this bin's // runtime slot. namerStmts.push_back(makeNamer(cpVarp, cbinp, -1)); const ConvBinTarget tgt{cpVarp, idx, cbinp->binsType().binIsNormal()}; for (AstNode* sp = cbinp->transp(); sp; sp = sp->nextp()) generateSingleTransitionCode(coverpointp, cbinp, exprp, tgt, VN_AS(sp, CoverTransSet)); ++idx; continue; } if (cbinp->isArray()) { // value array: bins b[N] = {...} -> b[0]..b[N-1] bool unsupported = false; std::vector values = extractArrayValues(cbinp, exprp, unsupported); if (unsupported) continue; // bin ignored (COVERIGN emitted); reserve no slot namerStmts.push_back(makeNamer(cpVarp, cbinp, static_cast(values.size()))); for (AstNodeExpr* valuep : values) { // TODO: A 4-state bin value (e.g. bins b[] = {2'b0x}) must match with === // (AstEqCase) per IEEE 1800-2023 19.5.4. == is equivalent under 2-state sim // (x/z collapse to 0); switch to AstEqCase when 4-state sim support lands. emitConvHitIf(coverpointp, cbinp, cpVarp, idx++, new AstEq{cbinp->fileline(), exprp->cloneTree(false), valuep}); } } else { namerStmts.push_back(makeNamer(cpVarp, cbinp, -1)); // buildBinCondition is null for 'ignore_bins = default' (no ranges); the bin // still gets a reserved slot (recorded, never incremented). if (AstNodeExpr* const condp = buildBinCondition(cbinp, exprp)) emitConvHitIf(coverpointp, cbinp, cpVarp, idx, condp); ++idx; } } for (AstCoverBin* const defBinp : defaultBins) { namerStmts.push_back(makeNamer(cpVarp, defBinp, -1)); emitConvHitIf(coverpointp, defBinp, cpVarp, idx++, buildDefaultCondition(coverpointp, exprp, defBinp->fileline())); } // Transition coverpoints track the previous sampled value; update it once at the end of // this coverpoint's sample() contribution (the prev var was created on demand by the // transition matching above). if (coverpointHasTransition(coverpointp)) { AstVar* const prevVarp = VN_AS(coverpointp->user1p(), Var); m_sampleFuncp->addStmtsp( new AstAssign{coverpointp->fileline(), new AstVarRef{prevVarp->fileline(), prevVarp, VAccess::WRITE}, exprp->cloneTree(false)}); } // Constructor: init (allocates), namers, then registration (under --coverage). // Under --protect-ids the hierarchy and page string reach the coverage database // verbatim, so obfuscate them like line/toggle points (per-word hierarchy, whole- // unit page). No-ops when --protect-ids is off. const bool prot = v3Global.opt.protectIds(); const std::string hier = VIdProtect::protectWordsIf(m_covergroupp->name() + "." + coverpointp->name(), prot); AstCStmt* const initp = new AstCStmt{fl}; initp->add(memberRef(fl, cpVarp)); initp->add(".init(\"" + hier + "\", " + std::to_string(atLeastValue) + ", " + std::to_string(idx) + ");"); m_constructorp->addStmtsp(initp); for (AstCStmt* const ns : namerStmts) m_constructorp->addStmtsp(ns); if (v3Global.opt.coverage()) { const std::string page = VIdProtect::protectIf("v_covergroup/" + m_covergroupp->name(), prot); AstCStmt* const regp = new AstCStmt{fl}; regp->add(memberRef(fl, cpVarp)); regp->add(".registerBins(vlSymsp->_vm_contextp__->coveragep(), \"" + page + "\");"); m_constructorp->addStmtsp(regp); } } // Generate state machine code for multi-value transition sequences // Handles transitions like (1 => 2 => 3 => 4) void generateMultiValueTransitionCode(AstCoverpoint* coverpointp, AstCoverBin* binp, AstNodeExpr* exprp, const ConvBinTarget& tgt, const std::vector& items) { UINFO(4, " Generating multi-value transition state machine for: " << binp->name()); UINFO(4, " Sequence length: " << items.size() << " items"); // Create state position variable AstVar* const stateVarp = createSequenceStateVar(coverpointp, binp); // Build case statement with N cases (one for each state 0 to N-1) // State 0: Not started, looking for first item // State 1 to N-1: In progress, looking for next item AstCase* const casep = new AstCase{binp->fileline(), VCaseType::CT_CASE, new AstVarRef{stateVarp->fileline(), stateVarp, VAccess::READ}, nullptr}; // Generate each case item in the switch statement for (size_t state = 0; state < items.size(); ++state) { AstCaseItem* caseItemp = generateTransitionStateCase(coverpointp, binp, exprp, tgt, stateVarp, items, state); casep->addItemsp(caseItemp); } // Add default case (reset to state 0) to prevent CASEINCOMPLETE warnings, // since the state variable is wider than the number of valid states. AstCaseItem* const defaultItemp = new AstCaseItem{ binp->fileline(), nullptr, new AstAssign{binp->fileline(), new AstVarRef{binp->fileline(), stateVarp, VAccess::WRITE}, new AstConst{binp->fileline(), AstConst::WidthedValue{}, 8, 0}}}; casep->addItemsp(defaultItemp); m_sampleFuncp->addStmtsp(casep); UINFO(4, " Successfully added multi-value transition state machine"); } // Generate code for a single state in the transition state machine // Returns the case item for this state AstCaseItem* generateTransitionStateCase(AstCoverpoint* coverpointp, AstCoverBin* binp, AstNodeExpr* exprp, const ConvBinTarget& tgt, AstVar* stateVarp, const std::vector& items, size_t state) { FileLine* const fl = binp->fileline(); // Build condition for current value matching expected item at this state AstNodeExpr* matchCondp = buildTransitionItemCondition(items[state], exprp); // Apply iff condition if present if (AstNodeExpr* iffp = coverpointp->iffp()) { matchCondp = new AstAnd{fl, iffp->cloneTree(false), matchCondp}; } AstNodeStmt* matchActionp = nullptr; if (state == items.size() - 1) { // Last state: sequence complete! Record the hit in the runtime VlCoverpoint. matchActionp = makeRuntimeBinHit(fl, tgt); // For illegal_bins, add error message if (binp->binsType() == VCoverBinsType::BINS_ILLEGAL) { const string errMsg = "Illegal transition bin " + binp->prettyNameQ() + " hit in coverpoint " + coverpointp->prettyNameQ(); matchActionp = matchActionp->addNext(makeIllegalBinAction(fl, errMsg)); } // Reset state to 0 matchActionp = matchActionp->addNext( new AstAssign{fl, new AstVarRef{fl, stateVarp, VAccess::WRITE}, new AstConst{fl, AstConst::WidthedValue{}, 8, 0}}); } else { // Intermediate state: advance to next state matchActionp = new AstAssign{ fl, new AstVarRef{fl, stateVarp, VAccess::WRITE}, new AstConst{fl, AstConst::WidthedValue{}, 8, static_cast(state + 1)}}; } // Build restart logic: check if current value matches first item // If so, restart sequence from state 1 (even if we're in middle of sequence) AstNodeStmt* noMatchActionp = nullptr; if (state > 0) { // Check if current value matches first item (restart condition) AstNodeExpr* restartCondp = buildTransitionItemCondition(items[0], exprp); UASSERT_OBJ(restartCondp, items[0], "buildTransitionItemCondition returned nullptr for restart"); // Apply iff condition if (AstNodeExpr* iffp = coverpointp->iffp()) { restartCondp = new AstAnd{fl, iffp->cloneTree(false), restartCondp}; } // Restart to state 1 AstNodeStmt* restartActionp = new AstAssign{fl, new AstVarRef{fl, stateVarp, VAccess::WRITE}, new AstConst{fl, AstConst::WidthedValue{}, 8, 1}}; // Reset to state 0 (else branch) AstNodeStmt* resetActionp = new AstAssign{fl, new AstVarRef{fl, stateVarp, VAccess::WRITE}, new AstConst{fl, AstConst::WidthedValue{}, 8, 0}}; noMatchActionp = new AstIf{fl, restartCondp, restartActionp, resetActionp}; } // For state 0, no action needed if no match (stay in state 0) // Combine into if-else AstNodeStmt* const stmtp = new AstIf{fl, matchCondp, matchActionp, noMatchActionp}; // Create case item for this state value AstCaseItem* const caseItemp = new AstCaseItem{ fl, new AstConst{fl, AstConst::WidthedValue{}, 8, static_cast(state)}, stmtp}; return caseItemp; } // Create: $error(msg); $stop; Used when an illegal bin is hit. AstNodeStmt* makeIllegalBinAction(FileLine* fl, const string& errMsg) { AstDisplay* const errorp = new AstDisplay{fl, VDisplayType::DT_ERROR, errMsg, nullptr, nullptr}; errorp->fmtp()->timeunit(m_covergroupp->timeunit()); static_cast(errorp)->addNext(new AstStop{fl, true}); return errorp; } // Clone a constant node, widening to targetWidth if needed (zero-extend). // Used to ensure comparisons use matching widths after V3Width has run. static AstConst* widenConst(FileLine* fl, AstConst* constp, int targetWidth) { if (constp->width() == targetWidth) return constp->cloneTree(false); V3Number num{fl, targetWidth, 0}; num.opAssign(constp->num()); return new AstConst{fl, num}; } // Build a range condition: minp <= exprp <= maxp. // Uses signed comparisons if exprp is signed; omits trivially-true bounds for unsigned. // All arguments are non-owning; clones exprp/minp/maxp as needed. AstNodeExpr* makeRangeCondition(FileLine* fl, AstNodeExpr* exprp, AstNodeExpr* minp, AstNodeExpr* maxp) { const int exprWidth = exprp->widthMin(); AstConst* const minConstp = VN_AS(minp, Const); AstConst* const maxConstp = VN_AS(maxp, Const); // Widen constants to match expression width so post-V3Width nodes use correct macros AstConst* const minWidep = widenConst(fl, minConstp, exprWidth); AstConst* const maxWidep = widenConst(fl, maxConstp, exprWidth); if (exprp->isSigned()) { return new AstAnd{fl, new AstGteS{fl, exprp->cloneTree(false), minWidep}, new AstLteS{fl, exprp->cloneTree(false), maxWidep}}; } // Unsigned: skip bounds that are trivially satisfied for the expression width const bool skipLowerCheck = (minConstp->toUQuad() == 0); bool skipUpperCheck = false; if (exprWidth <= 64) { const uint64_t maxVal = (exprWidth == 64) ? ~static_cast(0) : ((1ULL << exprWidth) - 1ULL); skipUpperCheck = (maxConstp->toUQuad() == maxVal); } if (skipLowerCheck && skipUpperCheck) { VL_DO_DANGLING(pushDeletep(minWidep), minWidep); VL_DO_DANGLING(pushDeletep(maxWidep), maxWidep); return new AstConst{fl, AstConst::BitTrue{}}; } else if (skipLowerCheck) { VL_DO_DANGLING(pushDeletep(minWidep), minWidep); return new AstLte{fl, exprp->cloneTree(false), maxWidep}; } else if (skipUpperCheck) { VL_DO_DANGLING(pushDeletep(maxWidep), maxWidep); return new AstGte{fl, exprp->cloneTree(false), minWidep}; } else { return new AstAnd{fl, new AstGte{fl, exprp->cloneTree(false), minWidep}, new AstLte{fl, exprp->cloneTree(false), maxWidep}}; } } // Build a one-sided comparison for an open-ended bin range whose other bound is '$'. // '$' denotes the coverpoint domain extreme, so {[lo:$]} == (expr >= lo) and // {[$:hi]} == (expr <= hi). AstNodeExpr* makeOpenRangeCondition(FileLine* fl, AstNodeExpr* exprp, AstConst* boundp, bool isLowerBound) { AstConst* const widep = widenConst(fl, boundp, exprp->widthMin()); if (isLowerBound) { if (exprp->isSigned()) return new AstGteS{fl, exprp->cloneTree(false), widep}; return new AstGte{fl, exprp->cloneTree(false), widep}; } if (exprp->isSigned()) return new AstLteS{fl, exprp->cloneTree(false), widep}; return new AstLte{fl, exprp->cloneTree(false), widep}; } // Build condition for a single transition item. // Returns expression that checks if exprp matches the item's value/range list. // Overload for when the expression is a variable read -- creates and manages the VarRef // internally, so callers don't need to construct a temporary node. AstNodeExpr* buildTransitionItemCondition(AstCoverTransItem* itemp, AstVar* varp) { AstNodeExpr* varRefp = new AstVarRef{varp->fileline(), varp, VAccess::READ}; AstNodeExpr* const condp = buildTransitionItemCondition(itemp, varRefp); VL_DO_DANGLING(pushDeletep(varRefp), varRefp); return condp; } // Non-owning: exprp is cloned internally; caller retains ownership of exprp. AstNodeExpr* buildTransitionItemCondition(AstCoverTransItem* itemp, AstNodeExpr* exprp) { AstNodeExpr* condp = nullptr; for (AstNode* valp = itemp->valuesp(); valp; valp = valp->nextp()) { AstNodeExpr* singleCondp = nullptr; AstConst* const constp = VN_AS(valp, Const); singleCondp = new AstEq{constp->fileline(), exprp->cloneTree(false), constp->cloneTree(false)}; if (condp) { condp = new AstOr{itemp->fileline(), condp, singleCondp}; } else { condp = singleCondp; } } return condp; } // Generate code for a single transition sequence (used by both regular and array bins) void generateSingleTransitionCode(AstCoverpoint* coverpointp, AstCoverBin* binp, AstNodeExpr* exprp, const ConvBinTarget& tgt, AstCoverTransSet* transSetp) { UINFO(4, " Generating code for transition sequence"); // Get or create previous value variable AstVar* const prevVarp = createPrevValueVar(coverpointp, exprp); UASSERT_OBJ( transSetp, binp, "Transition bin has no transition set (transp() was checked before calling this)"); // Get transition items (the sequence: item1 => item2 => item3) std::vector items; for (AstNode* itemp = transSetp->itemsp(); itemp; itemp = itemp->nextp()) items.push_back(VN_AS(itemp, CoverTransItem)); if (items.empty()) { binp->v3error("Transition set without items"); return; } if (items.size() == 1) { // Single item transition not valid (need at least 2 values for =>) binp->v3error("Transition requires at least two values"); return; } else if (items.size() == 2) { // Simple two-value transition: (val1 => val2) // Use optimized direct comparison (no state machine needed) AstNodeExpr* const cond1p = buildTransitionItemCondition(items[0], prevVarp); AstNodeExpr* const cond2p = buildTransitionItemCondition(items[1], exprp); // Combine: prev matches val1 AND current matches val2 AstNodeExpr* fullCondp = new AstAnd{binp->fileline(), cond1p, cond2p}; addConvTransHitIf(coverpointp, binp, tgt, fullCondp); UINFO(4, " Successfully added 2-value transition if statement"); } else { // Multi-value sequence (a => b => c => ...) // Use state machine to track position in sequence generateMultiValueTransitionCode(coverpointp, binp, exprp, tgt, items); } } // Append a "{ VlCoverpoint* __Vcx_cps[] = {&cp0, &cp1, ...}; . }" statement. AstCStmt* makeCrossCpsCall(FileLine* fl, const std::vector& cpVars, AstVar* cxVarp, const std::string& callText) { AstCStmt* const cs = new AstCStmt{fl}; cs->add("{ VlCoverpoint* __Vcx_cps[] = {"); for (size_t d = 0; d < cpVars.size(); ++d) { cs->add(d == 0 ? "&" : ", &"); cs->add(memberRef(fl, cpVars[d])); } cs->add("}; "); cs->add(memberRef(fl, cxVarp)); cs->add(callText); cs->add(" }"); return cs; } // Route a cross through a VlCoverCross member: emit the member, its constructor init + // registration, and the sample() call. The feeding coverpoints are already generated // (their hit lists drive the cross), so only O(1) generated code is needed here. void generateCross(AstCoverCross* crossp) { FileLine* const fl = crossp->fileline(); UINFO(4, " Generating VlCoverCross member: " << crossp->name()); if (AstNodeExpr* const iffp = crossp->iffp()) { crossp->iffp( captureIffToTemp(iffp, "__VcrossIff_" + sanitizeGeneratedName(crossp->name()))); } // Resolve and unlink the coverpoint refs, in dimension order. Every ref resolves to a // known coverpoint (a cross with an unresolvable item was dropped earlier). std::vector cpVars; for (AstNode* itemp = crossp->itemsp(); itemp;) { AstNode* const nextp = itemp->nextp(); AstCoverpointRef* const refp = VN_AS(itemp, CoverpointRef); const auto it = m_cpVarMap.find(refp->name()); UASSERT_OBJ(it != m_cpVarMap.end(), crossp, "Cross references an unknown coverpoint"); cpVars.push_back(it->second); VL_DO_DANGLING(pushDeletep(refp->unlinkFrBack()), refp); itemp = nextp; } const int dims = static_cast(cpVars.size()); if (!m_vlCoverCrossDTypep) { m_vlCoverCrossDTypep = new AstCDType{fl, "VlCoverCross"}; v3Global.rootp()->typeTablep()->addTypesp(m_vlCoverCrossDTypep); } AstVar* const cxVarp = new AstVar{fl, VVarType::MEMBER, "__Vcx_" + crossp->name(), m_vlCoverCrossDTypep}; cxVarp->isStatic(false); m_covergroupp->addMembersp(cxVarp); m_crossVars.push_back(cxVarp); // Constructor: init (after the coverpoints, which generate earlier) then registration. // Obfuscate the hierarchy/filename/page under --protect-ids as for coverpoints above. const bool prot = v3Global.opt.protectIds(); const std::string hier = VIdProtect::protectWordsIf(m_covergroupp->name() + "." + crossp->name(), prot); const std::string initCall = ".init(\"" + hier + "\", " + std::to_string(dims) + ", __Vcx_cps, \"" + VIdProtect::protectIf(fl->filename(), prot) + "\", " + std::to_string(fl->lineno()) + ", " + std::to_string(fl->firstColumn()) + ");"; m_constructorp->addStmtsp(makeCrossCpsCall(fl, cpVars, cxVarp, initCall)); if (v3Global.opt.coverage()) { const std::string page = VIdProtect::protectIf("v_covergroup/" + m_covergroupp->name(), prot); AstCStmt* const regp = new AstCStmt{fl}; regp->add(memberRef(fl, cxVarp)); regp->add(".registerBins(vlSymsp->_vm_contextp__->coveragep(), \"" + page + "\");"); m_constructorp->addStmtsp(regp); } // sample(): after all coverpoints have sampled (cross loop runs after coverpoint loop). UASSERT_OBJ(m_sampleFuncp, crossp, "sample() CFunc not set for cross"); AstNodeStmt* const samplep = makeCrossCpsCall(fl, cpVars, cxVarp, ".sample(__Vcx_cps);"); if (AstNodeExpr* const iffp = crossp->iffp()) { m_sampleFuncp->addStmtsp(new AstIf{fl, iffp->cloneTree(false), samplep}); } else { m_sampleFuncp->addStmtsp(samplep); } } void generateCrossCode(AstCoverCross* crossp) { UINFO(4, " Generating code for cross: " << crossp->name()); // Non-standard hierarchical/dotted cross item (e.g. 'cross a.b'): an implicit coverpoint // over the referenced expression (carried in refp->exprp()). The grammar already warned // NONSTD; implicit coverpoints are not yet implemented, so generate no sampling code for // this cross. When support is added the implicit coverpoint should be synthesized // upstream (V3LinkParse) as a real AstCoverpoint so it flows through the normal coverpoint // path - by here coverpoint lowering has already run. for (AstNode* itemp = crossp->itemsp(); itemp; itemp = itemp->nextp()) { const AstCoverpointRef* const refp = VN_AS(itemp, CoverpointRef); if (refp->exprp()) { refp->v3warn(COVERIGN, "Unsupported: cross of hierarchical reference (implicit coverpoint)"); return; } } // A cross naming a bare variable (implicit coverpoint, which Verilator does not // synthesize) is dropped entirely with a COVERIGN warning -- it produces no coverage // either way -- but only this cross is dropped; its sibling crosses are still generated // and the real coverpoints it referenced remain as independent coverpoints. if (m_droppedCrosses.count(crossp)) { for (AstNode* itemp = crossp->itemsp(); itemp; itemp = itemp->nextp()) { const AstCoverpointRef* const refp = VN_AS(itemp, CoverpointRef); if (m_coverpointMap.find(refp->name()) == m_coverpointMap.end()) { refp->v3warn(COVERIGN, "Unsupported: cross of " << refp->prettyNameQ() << " which is not a coverpoint (implicit " "coverpoint)"); break; } } return; } // Every cross that isn't dropped routes through a VlCoverCross member. generateCross(crossp); } AstNodeExpr* buildBinCondition(AstCoverBin* binp, AstNodeExpr* exprp) { // Get the range list from the bin AstNode* const rangep = binp->rangesp(); if (!rangep) return nullptr; // Check if this is a wildcard bin const bool isWildcard = binp->isWildcard(); // Build condition by OR-ing all ranges together AstNodeExpr* fullCondp = nullptr; for (AstNode* currRangep = rangep; currRangep; currRangep = currRangep->nextp()) { AstNodeExpr* rangeCondp = nullptr; if (AstInsideRange* irp = VN_CAST(currRangep, InsideRange)) { AstNodeExpr* const minExprp = irp->lhsp(); AstNodeExpr* const maxExprp = irp->rhsp(); AstConst* const minConstp = VN_CAST(minExprp, Const); AstConst* const maxConstp = VN_CAST(maxExprp, Const); const bool loUnbounded = VN_IS(minExprp, Unbounded); const bool hiUnbounded = VN_IS(maxExprp, Unbounded); if (loUnbounded || hiUnbounded) { // Open-ended range: '$' is the coverpoint domain min/max, so the // range reduces to a single inequality (e.g. {[10:$]} -> expr >= 10). AstConst* const boundp = hiUnbounded ? minConstp : maxConstp; if (loUnbounded && hiUnbounded) { rangeCondp = new AstConst{irp->fileline(), AstConst::BitTrue{}}; } else if (!boundp) { irp->v3error("Non-constant expression in bin range; " "range bounds must be constants"); return nullptr; } else if (boundp->num().isFourState()) { irp->v3error("Four-state (x/z) value in bin range bound; " "range bounds must be two-state constants"); return nullptr; } else { rangeCondp = makeOpenRangeCondition(irp->fileline(), exprp, boundp, /*isLowerBound=*/hiUnbounded); } } else if (!minConstp || !maxConstp) { irp->v3error("Non-constant expression in bin range; " "range bounds must be constants"); return nullptr; } else if (minConstp->num().isFourState() || maxConstp->num().isFourState()) { irp->v3error("Four-state (x/z) value in bin range bound; " "range bounds must be two-state constants"); return nullptr; } else if (minConstp->toUQuad() == maxConstp->toUQuad()) { // Single value if (isWildcard) { rangeCondp = buildWildcardCondition(binp, exprp, minConstp); } else { rangeCondp = new AstEq{binp->fileline(), exprp->cloneTree(false), minExprp->cloneTree(false)}; } } else { rangeCondp = makeRangeCondition(irp->fileline(), exprp, minExprp, maxExprp); } } else if (AstConst* constp = VN_CAST(currRangep, Const)) { if (isWildcard) { rangeCondp = buildWildcardCondition(binp, exprp, constp); } else { // TODO: A 4-state bin value (e.g. bins b = {2'b0x}) must match with === // (AstEqCase) per IEEE 1800-2023 19.5.4. == is equivalent under 2-state sim // (x/z collapse to 0); switch to AstEqCase when 4-state sim support lands. rangeCondp = new AstEq{binp->fileline(), exprp->cloneTree(false), constp->cloneTree(false)}; } } else { currRangep->v3error( "Non-constant expression in bin range; values must be constants"); return nullptr; } UASSERT_OBJ(rangeCondp, binp, "rangeCondp is null after building range condition"); fullCondp = fullCondp ? new AstOr{binp->fileline(), fullCondp, rangeCondp} : rangeCondp; } return fullCondp; } // Build a wildcard condition: (expr & mask) == (value & mask) // where mask has 1s for defined bits and 0s for wildcard bits // Non-owning: exprp is cloned internally; caller retains ownership. AstNodeExpr* buildWildcardCondition(AstCoverBin* binp, AstNodeExpr* exprp, AstConst* constp) { FileLine* const fl = binp->fileline(); // Extract mask from constant (bits that are not X/Z) V3Number mask{constp, constp->width()}; V3Number value{constp, constp->width()}; for (int bit = 0; bit < constp->width(); ++bit) { if (constp->num().bitIs0(bit) || constp->num().bitIs1(bit)) { mask.setBit(bit, 1); value.setBit(bit, constp->num().bitIs1(bit) ? 1 : 0); } else { mask.setBit(bit, 0); value.setBit(bit, 0); } } // Generate: (expr & mask) == (value & mask) AstConst* const maskConstp = new AstConst{fl, mask}; AstConst* const valueConstp = new AstConst{fl, value}; AstNodeExpr* const exprMasked = new AstAnd{fl, exprp->cloneTree(false), maskConstp}; AstNodeExpr* const valueMasked = new AstAnd{fl, valueConstp, maskConstp->cloneTree(false)}; // TODO: masking the wildcard (don't-care) bits is correct, but the defined-bit // comparison should use === (AstEqCase) per IEEE 1800-2023 19.5.4 once 4-state sim // support lands; == is equivalent under 2-state sim (x/z collapse to 0). return new AstEq{fl, exprMasked, valueMasked}; } void generateCoverageComputationCode() { UINFO(4, " Generating coverage computation code"); // Invalidate cache: addMembersp() calls in generateCoverpointCode/generateCrossCode // have added new members since the last scan, so clear before re-querying. m_memberMap.clear(); // Find get_coverage() and get_inst_coverage() methods AstFunc* const getCoveragep = VN_CAST(m_memberMap.findMember(m_covergroupp, "get_coverage"), Func); AstFunc* const getInstCoveragep = VN_CAST(m_memberMap.findMember(m_covergroupp, "get_inst_coverage"), Func); // Generate code for get_inst_coverage() (an empty covergroup returns 100%). generateCoverageMethodBody(getInstCoveragep); // Generate code for get_coverage() (type-level) // NOTE: Full type-level coverage requires instance tracking infrastructure // For now, return 0.0 as a placeholder AstVar* const coverageReturnVarp = VN_AS(getCoveragep->fvarp(), Var); // TODO: Implement proper type-level coverage aggregation // This requires tracking all instances and averaging their coverage // For now, return 0.0 getCoveragep->addStmtsp(new AstAssign{ getCoveragep->fileline(), new AstVarRef{getCoveragep->fileline(), coverageReturnVarp, VAccess::WRITE}, new AstConst{getCoveragep->fileline(), AstConst::RealDouble{}, 0.0}}); UINFO(4, " Added placeholder get_coverage() (returns 0.0)"); } void generateCoverageMethodBody(AstFunc* funcp) { FileLine* const fl = funcp->fileline(); AstVar* const returnVarp = VN_AS(funcp->fvarp(), Var); // Every coverpoint and cross holds its bins in the runtime (VlCoverpoint/VlCoverCross). // Sum their covered/total contributions via coverageParts (Normal bins only; ignore, // illegal, and default are excluded per LRM 19.5). A covergroup with no coverpoints // (and hence no crosses) has nothing to cover and reports 100%. if (m_cpVars.empty()) { funcp->addStmtsp(new AstAssign{fl, new AstVarRef{fl, returnVarp, VAccess::WRITE}, new AstConst{fl, AstConst::RealDouble{}, 100.0}}); return; } AstCStmt* const headp = new AstCStmt{fl}; headp->add("double __Vcov = 0.0; double __Vtot = 0.0;"); funcp->addStmtsp(headp); for (AstVar* const cpVarp : m_cpVars) { AstCStmt* const cs = new AstCStmt{fl}; cs->add("{ double __Vc = 0.0; double __Vt = 0.0; "); cs->add(memberRef(fl, cpVarp)); cs->add(".coverageParts(__Vc, __Vt); __Vcov += __Vc; __Vtot += __Vt; }"); funcp->addStmtsp(cs); } // Crosses contribute the same covered/total ratio as their per-tuple bins. for (AstVar* const cxVarp : m_crossVars) { AstCStmt* const cs = new AstCStmt{fl}; cs->add("{ double __Vc = 0.0; double __Vt = 0.0; "); cs->add(memberRef(fl, cxVarp)); cs->add(".coverageParts(__Vc, __Vt); __Vcov += __Vc; __Vtot += __Vt; }"); funcp->addStmtsp(cs); } AstCStmt* const retp = new AstCStmt{fl}; retp->add(new AstVarRef{fl, returnVarp, VAccess::WRITE}); retp->add(" = (__Vtot != 0.0) ? (100.0 * __Vcov / __Vtot) : 100.0;"); funcp->addStmtsp(retp); } // VISITORS static bool isEnclosingInstanceVar(const AstVar* varp) { return varp->isClassMember() && !varp->lifetime().isStatic() && !varp->isParam(); } void rewriteThisRef(AstThisRef* refp, AstVar* handleVarp) { const AstClassRefDType* const refDTypep = VN_CAST(refp->dtypep()->skipRefp(), ClassRefDType); UASSERT_OBJ(refDTypep && refDTypep->classp() == m_covergroupp, refp, "Unexpected this reference in embedded covergroup"); AstNodeExpr* const newp = new AstVarRef{refp->fileline(), handleVarp, VAccess::READ}; refp->replaceWith(newp); VL_DO_DANGLING(pushDeletep(refp), refp); } void rewriteVarRef(AstVarRef* refp, AstVar* handleVarp) { FileLine* const fl = refp->fileline(); AstMemberSel* const selp = new AstMemberSel{fl, new AstVarRef{fl, handleVarp, VAccess::READ}, refp->varp()}; selp->access(refp->access()); refp->replaceWith(selp); VL_DO_DANGLING(pushDeletep(refp), refp); } 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; } } // V3LinkParse always creates an implicit variable for an embedded covergroup. return nullptr; // LCOV_EXCL_LINE } std::vector findCovergroupConstructions() { 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); const AstVarRef* const lhsRefp = VN_CAST(asgnp->lhsp(), VarRef); if (!newp || !lhsRefp || lhsRefp->varp() != m_embeddedVarp) return; const AstClassRefDType* const refp = VN_CAST(newp->dtypep(), ClassRefDType); if (refp && refp->classp() == m_covergroupp) foundps.push_back(asgnp); }); return foundps; } AstNodeAssign* findInvalidEmbeddedCovergroupAssignment() { if (!m_embeddedVarp) return nullptr; std::set constructorAssignps; AstFunc* const enclosingNewp = VN_CAST(m_memberMap.findMember(m_enclosingClassp, "new"), Func); if (enclosingNewp) { enclosingNewp->foreach([&](AstNodeAssign* asgnp) { const AstVarRef* const refp = VN_CAST(asgnp->lhsp(), VarRef); if (refp && refp->varp() == m_embeddedVarp) constructorAssignps.insert(asgnp); }); } AstNodeAssign* invalidp = nullptr; m_enclosingClassp->foreach([&](AstNodeAssign* asgnp) { if (invalidp || constructorAssignps.count(asgnp)) return; const AstVarRef* const refp = VN_CAST(asgnp->lhsp(), VarRef); if (refp && refp->varp() == m_embeddedVarp) invalidp = asgnp; }); return invalidp; } std::set enclosingInstanceVars() const { std::set vars; if (m_enclosingClassp) { m_enclosingClassp->foreachMember([&](AstClass* const, AstVar* const varp) { if (isEnclosingInstanceVar(varp)) vars.insert(varp); }); } return vars; } bool hasEnclosingEventRef(AstCovergroup* cgp) const { if (!m_embeddedVarp || !cgp->eventp()) return false; const std::set enclosingVars = enclosingInstanceVars(); bool found = false; cgp->eventp()->foreach([&](AstVarRef* refp) { if (enclosingVars.count(refp->varp())) found = true; }); return found; } static bool parseEmbeddedEventExpr(AstNodeExpr* exprp, AstVar*& baseVarp, AstVar*& memberVarp) { if (AstVarRef* const refp = VN_CAST(exprp, VarRef)) { 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) return false; baseVarp = baseRefp->varp(); memberVarp = selp->varp(); return true; } bool isEventLvalue(AstNodeExpr* exprp, const EmbeddedEventTrigger& trigger) const { if (AstSel* const selp = VN_CAST(exprp, Sel)) exprp = selp->fromp(); 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) const { 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; } string eventPrevName(const EmbeddedEventTrigger& trigger, size_t triggerIndex) const { string name = "__Vcg_prev_" + m_embeddedVarp->name() + "_" + std::to_string(triggerIndex) + "_" + trigger.baseVarp->name(); if (trigger.memberVarp) name += "_" + trigger.memberVarp->name(); return name; } AstNodeExpr* newEmbeddedVarNonNull(FileLine* fl) const { return new AstNeq{fl, new AstVarRef{fl, m_embeddedVarp, VAccess::READ}, new AstConst{fl, AstConst::Null{}}}; } AstNodeStmt* newSampleStmt(FileLine* fl) const { AstMethodCall* const callp = new AstMethodCall{ fl, new AstVarRef{fl, m_embeddedVarp, VAccess::READ}, "sample", nullptr}; callp->taskp(m_sampleFuncp); callp->dtypeSetVoid(); return callp->makeStmt(); } void installEmbeddedEventFork(AstSenTree* eventp, const std::vector& constructps) { // IEEE 1800-2023 19.3 samples coverpoints whenever their clocking event occurs. A // per-instance event cannot use V3Active's static sensitivity path, so spawn // 'fork forever begin @(event); cg.sample(); end join_none' after each construction. 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->immediateStart(true); forkp->addForksp(new AstBegin{fl, "", loopp, true}); constructp->addNextHere(forkp); } VL_DO_DANGLING(pushDeletep(eventp), eventp); } AstNodeExpr* newEventReadyCondition(FileLine* fl, const EmbeddedEventTrigger& trigger) const { AstNodeExpr* const curp = newEventRead(fl, trigger); AstNodeExpr* const prevp = new AstVarRef{fl, trigger.prevVarp, VAccess::READ}; AstNodeExpr* edgep = nullptr; // IEEE 1800-2023 9.4.2 detects edge-qualified events only on the expression's LSB, // while an implicit change event observes the complete expression. if (trigger.edgeType == VEdgeType::ET_POSEDGE) { edgep = new AstSel{fl, new AstAnd{fl, curp, new AstNot{fl, prevp}}, 0, 1}; } else if (trigger.edgeType == VEdgeType::ET_NEGEDGE) { edgep = new AstSel{fl, new AstAnd{fl, new AstNot{fl, curp}, prevp}, 0, 1}; } else if (trigger.edgeType == VEdgeType::ET_BOTHEDGE) { edgep = new AstSel{fl, new AstXor{fl, curp, prevp}, 0, 1}; } else { edgep = new AstNeq{fl, curp, prevp}; } return new AstLogAnd{fl, newEmbeddedVarNonNull(fl), edgep}; } std::vector collectEmbeddedEventTriggers(AstCovergroup* cgp) { std::vector triggers; const std::set enclosingVars = enclosingInstanceVars(); for (AstNode* senp = cgp->eventp()->sensesp(); senp; senp = senp->nextp()) { AstSenItem* const itemp = VN_AS(senp, SenItem); AstVar* baseVarp = nullptr; AstVar* memberVarp = nullptr; if (!parseEmbeddedEventExpr(itemp->sensp(), baseVarp, memberVarp) || !enclosingVars.count(baseVarp)) { return {}; } triggers.emplace_back(itemp->fileline(), baseVarp, memberVarp, itemp->edgeType()); } return triggers; } void installEmbeddedEventTriggers(std::vector& triggers, const std::vector& constructps) { // Without --timing, approximate a per-instance event by sampling after assignments // within the enclosing class. External writes and exact scheduling cannot be observed. if (constructps.empty()) return; for (size_t triggerIndex = 0; triggerIndex < triggers.size(); ++triggerIndex) { EmbeddedEventTrigger& trigger = triggers[triggerIndex]; 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. Use --timing for " "full support."); continue; } AstNodeDType* const dtypep = trigger.memberVarp ? trigger.memberVarp->dtypep() : trigger.baseVarp->dtypep(); AstVar* const prevVarp = new AstVar{trigger.eventFl, VVarType::MEMBER, eventPrevName(trigger, triggerIndex), dtypep}; prevVarp->isStatic(false); 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(new AstAssign{fl, new AstVarRef{fl, trigger.prevVarp, VAccess::WRITE}, newEventRead(fl, trigger)}); asgnp->addNextHere(ifp); } } } void deleteCoverageItems() { for (AstCoverpoint* const cpp : m_coverpoints) { VL_DO_DANGLING(pushDeletep(cpp->unlinkFrBack()), cpp); } for (AstCoverCross* const crossp : m_coverCrosses) { VL_DO_DANGLING(pushDeletep(crossp->unlinkFrBack()), crossp); } } class FormalRefVisitor final : public VNVisitor { const std::set& m_constructorArgs; const std::map& m_replacements; AstMemberSel* m_memberSelp = nullptr; AstNode* m_offenderp = nullptr; void visit(AstMemberSel* nodep) override { if (m_offenderp) return; VL_RESTORER(m_memberSelp); if (!m_memberSelp) m_memberSelp = nodep; iterateChildren(nodep); } void visit(AstVarRef* nodep) override { if (!m_memberSelp) return; const auto it = m_replacements.find(nodep->varp()); if (it != m_replacements.end()) { nodep->varp(it->second); } else if (m_constructorArgs.count(nodep->varp())) { m_offenderp = m_memberSelp; } } void visit(AstNode* nodep) override { if (!m_offenderp) iterateChildren(nodep); } public: FormalRefVisitor(const std::set& constructorArgs, const std::map& replacements) : m_constructorArgs{constructorArgs} , m_replacements{replacements} {} void scan(AstNode* nodep) { if (nodep && !m_offenderp) iterate(nodep); } AstNode* offenderp() const { return m_offenderp; } }; AstNode* findUnsupportedFormalRef() { std::set constructorArgs; std::map replacements; for (AstNode* stmtp = m_constructorp->stmtsp(); stmtp; stmtp = stmtp->nextp()) { AstVar* const varp = VN_CAST(stmtp, Var); if (!varp || !varp->isIO()) continue; constructorArgs.insert(varp); if (!VN_IS(varp->dtypep()->skipRefp(), ClassRefDType)) continue; AstVar* const memberp = VN_CAST(m_memberMap.findMember(m_covergroupp, varp->name()), Var); UASSERT_OBJ(memberp && memberp->isClassMember(), varp, "Covergroup constructor argument missing persistent member"); replacements.emplace(varp, memberp); } FormalRefVisitor visitor{constructorArgs, replacements}; for (AstCoverpoint* const cpp : m_coverpoints) { visitor.scan(cpp->exprp()); visitor.scan(cpp->iffp()); } for (AstCoverCross* const crossp : m_coverCrosses) visitor.scan(crossp->iffp()); return visitor.offenderp(); } AstVarRef* installEnclosingBackPointer(const std::vector& constructps) { // 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 an invalid // reference if an outer class member cannot be reached; otherwise returns an empty result. if (!m_enclosingClassp) return nullptr; // Offending refs require an enclosing class AstVarRef* invalidp = nullptr; AstNode* offenderp = nullptr; 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); } const std::set enclosingVars = enclosingInstanceVars(); std::vector refsToRewrite; std::vector thisRefsToRewrite; const auto scan = [&](AstNode* rootp) { rootp->foreach([&](AstVarRef* refp) { if (invalidp) return; const AstVar* const varp = refp->varp(); if (!isEnclosingInstanceVar(varp) || ownVars.count(varp)) return; if (!enclosingVars.count(varp)) { invalidp = refp; return; } refsToRewrite.push_back(refp); if (!offenderp) offenderp = refp; }); if (invalidp) return; rootp->foreach([&](AstThisRef* refp) { const AstClassRefDType* const refDTypep = VN_CAST(refp->dtypep()->skipRefp(), ClassRefDType); if (refDTypep && refDTypep->classp() == m_covergroupp) { thisRefsToRewrite.push_back(refp); if (!offenderp) offenderp = refp; } }); }; for (AstCoverpoint* const cpp : m_coverpoints) scan(cpp); for (AstCoverCross* const crossp : m_coverCrosses) scan(crossp); if (invalidp || !offenderp) return invalidp; UASSERT_OBJ(m_embeddedVarp, m_covergroupp, "Embedded covergroup variable not found"); // 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}; enclDTypep->rawPointer(true); 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 raw back-pointer after each construction. With no construction site, // the embedded covergroup handle remains null, so no back-pointer is observed. 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 thisp = new AstCExpr{cfl, "this"}; thisp->dtypep(enclDTypep); constructp->addNextHere(new AstAssign{cfl, lhsp, thisp}); } return nullptr; } 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_CLEAR(m_coverpoints); VL_RESTORER_CLEAR(m_coverpointMap); VL_RESTORER_CLEAR(m_coverCrosses); m_covergroupp = nodep; m_embeddedVarp = findEmbeddedCovergroupVar(); m_sampleFuncp = nullptr; m_constructorp = nullptr; std::vector embeddedEventTriggers; AstSenTree* embeddedEventForkp = nullptr; // Extract and store the clocking event from AstCovergroup node // The parser creates this node to preserve the event information bool hasUnsupportedEvent = false; for (AstNode* itemp = nodep->membersp(); itemp;) { AstNode* const nextp = itemp->nextp(); if (AstCovergroup* const cgp = VN_CAST(itemp, Covergroup)) { // Store the event in the global map for V3Active to retrieve later // V3LinkParse only creates this sentinel AstCovergroup node when a clocking // event exists, so cgp->eventp() is always non-null here. UASSERT_OBJ(cgp->eventp(), cgp, "Sentinel AstCovergroup in class must have non-null eventp"); if (hasEnclosingEventRef(cgp)) { UASSERT_OBJ(m_embeddedVarp, cgp, "Embedded covergroup event has no instance variable"); // IEEE 1800-2023 19.4 permits assignment to an embedded covergroup // variable only in the enclosing class's new method. if (AstNodeAssign* const invalidp = findInvalidEmbeddedCovergroupAssignment()) { invalidp->v3error( "Embedded covergroup variable " << m_embeddedVarp->prettyNameQ() << " may only be assigned in the enclosing class's 'new' method " "(IEEE 1800-2023 19.4)."); hasUnsupportedEvent = true; VL_DO_DANGLING(pushDeletep(cgp->unlinkFrBack()), cgp); itemp = nextp; continue; } 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; } } VL_DO_DANGLING(pushDeletep(cgp->unlinkFrBack()), cgp); itemp = nextp; continue; } // V3Active handles events that do not depend on an enclosing instance. UINFO(4, "Keeping covergroup event node for V3Active: " << nodep->name()); itemp = nextp; continue; } itemp = nextp; } // If covergroup has unsupported clocking event, skip processing it // but still clean up coverpoints so they don't reach downstream passes if (hasUnsupportedEvent) { iterateChildren(nodep); deleteCoverageItems(); return; } // Find the sample() method and constructor m_sampleFuncp = VN_CAST(m_memberMap.findMember(nodep, "sample"), Func); // V3LinkParse always synthesizes a sample() method for every covergroup, and the // sampling-code generation below dereferences m_sampleFuncp unconditionally. UASSERT_OBJ(m_sampleFuncp, nodep, "Covergroup missing synthesized sample() method"); m_sampleFuncp->isCovergroupSample(true); m_constructorp = VN_CAST(m_memberMap.findMember(nodep, "new"), Func); UINFO(9, "Found sample() method: " << (m_sampleFuncp ? "yes" : "no")); UINFO(9, "Found constructor: " << (m_constructorp ? "yes" : "no")); iterateChildren(nodep); if (AstNode* const offenderp = findUnsupportedFormalRef()) { offenderp->v3warn(COVERIGN, "Unsupported: 'covergroup' coverpoint dereferencing a " "non-class constructor argument; ignoring covergroup " << nodep->prettyNameQ()); deleteCoverageItems(); if (embeddedEventForkp) { VL_DO_DANGLING(pushDeletep(embeddedEventForkp), embeddedEventForkp); } return; } const std::vector constructps = findCovergroupConstructions(); // Embedded covergroups (IEEE 1800-2023 19.4): coverpoints, iff expressions, and // crosses may reference members of the enclosing class. The covergroup is lowered // into a sibling class with no implicit handle to the enclosing instance. Install // an explicit back-pointer and route the references through it. if (AstVarRef* const invalidp = installEnclosingBackPointer(constructps)) { invalidp->v3error("Non-static member " << invalidp->varp()->prettyNameQ() << " of an outer class requires an explicit " "object handle (IEEE 1800-2023 8.23)."); deleteCoverageItems(); if (embeddedEventForkp) { VL_DO_DANGLING(pushDeletep(embeddedEventForkp), embeddedEventForkp); } return; } installEmbeddedEventTriggers(embeddedEventTriggers, constructps); if (embeddedEventForkp) installEmbeddedEventFork(embeddedEventForkp, constructps); processCovergroup(); // Remove lowered coverpoints/crosses from the class - they have been // fully translated into C++ code and must not reach downstream passes deleteCoverageItems(); } 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); } } void visit(AstCoverpoint* nodep) override { UINFO(9, "Found coverpoint: " << nodep->name()); m_coverpoints.push_back(nodep); m_coverpointMap.emplace(nodep->name(), nodep); iterateChildren(nodep); } void visit(AstCoverCross* nodep) override { UINFO(9, "Found cross: " << nodep->name()); m_coverCrosses.push_back(nodep); iterateChildren(nodep); } void visit(AstNode* nodep) override { iterateChildren(nodep); } public: // CONSTRUCTORS explicit FunctionalCoverageVisitor(AstNetlist* nodep) { iterate(nodep); } ~FunctionalCoverageVisitor() override = default; }; //###################################################################### // Functional coverage class functions void V3Covergroup::covergroup(AstNetlist* nodep) { UINFO(4, __FUNCTION__ << ": "); { FunctionalCoverageVisitor{nodep}; } // Destruct before checking V3Global::dumpCheckGlobalTree("coveragefunc", 0, dumpTreeEitherLevel() >= 3); }