Files
verilator/src/V3Covergroup.cpp
T

4511 lines
230 KiB
C++

// -*- 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 "V3Error.h"
#include "V3File.h"
#include "V3MemberMap.h"
#include "V3UniqueNames.h"
#include <bitset>
#include <cmath>
#include <deque>
#include <set>
#include <tuple>
#include <unordered_map>
#include <vector>
VL_DEFINE_DEBUG_FUNCTIONS;
//######################################################################
// Embedded covergroup assignment validation
class CovergroupAssignValidVisitor final : public VNVisitorConst {
VMemberMap m_memberMap;
std::map<const AstVar*, const AstNodeFTask*>
m_constructors; // Implicit instance -> constructor
const AstNodeFTask* m_ftaskp = nullptr;
bool m_collecting = true;
bool m_valid = true;
void visit(AstClass* nodep) override {
VL_RESTORER(m_ftaskp);
m_ftaskp = nullptr;
if (m_collecting) {
const AstNodeFTask* const constructorp
= VN_CAST(m_memberMap.findMember(nodep, "new"), NodeFTask);
for (const AstNode* itemp = nodep->membersp(); itemp; itemp = itemp->nextp()) {
const AstVar* const varp = VN_CAST(itemp, Var);
// Only the implicit instance is restricted, not explicitly typed aliases.
if (!varp || !varp->isClassMember() || varp->isDeclTyped()) continue;
const AstClassRefDType* const refp
= VN_CAST(varp->dtypep()->skipRefp(), ClassRefDType);
if (refp && refp->classp()->covergroupEnclosingClassp() == nodep) {
m_constructors.emplace(varp, constructorp);
}
}
}
iterateChildrenConst(nodep);
}
void visit(AstNodeFTask* nodep) override {
VL_RESTORER(m_ftaskp);
m_ftaskp = nodep;
iterateChildrenConst(nodep);
}
void visit(AstNodeAssign* nodep) override {
if (!m_collecting) {
const AstVar* varp = nullptr;
if (const AstNodeVarRef* const refp = VN_CAST(nodep->lhsp(), NodeVarRef)) {
varp = refp->varp();
} else if (const AstMemberSel* const selp = VN_CAST(nodep->lhsp(), MemberSel)) {
varp = selp->varp();
}
const auto it = m_constructors.find(varp);
if (it != m_constructors.end() && (!m_ftaskp || m_ftaskp != it->second)) {
m_valid = false;
nodep->v3error("Embedded covergroup variable "
<< varp->prettyNameQ()
<< " may only be assigned in the enclosing class's 'new' method "
"(IEEE 1800-2023 19.4).");
}
}
iterateChildrenConst(nodep);
}
void visit(AstNode* nodep) override { iterateChildrenConst(nodep); }
public:
explicit CovergroupAssignValidVisitor(AstNetlist* nodep) {
// Uses can precede their enclosing class in the tree.
iterateConst(nodep);
m_collecting = false;
if (!m_constructors.empty()) iterateConst(nodep);
}
bool valid() const { return m_valid; }
};
//######################################################################
// Covergroup expression validation visitor
class CovergroupExprValidVisitor final : public VNVisitor {
const std::set<const AstVar*>& m_sampleMembers;
const std::set<const AstVar*>& m_constructorRefMembers;
bool m_inCoverageExpression = false;
bool m_sampleFormalAllowed = false;
void scanSampleExpression(AstNode* nodep) {
if (!nodep) return;
VL_RESTORER(m_sampleFormalAllowed);
m_sampleFormalAllowed = true;
iterateAndNextNull(nodep);
}
void scanCoverageExpression(AstNode* nodep) {
if (!nodep) return;
VL_RESTORER(m_inCoverageExpression);
m_inCoverageExpression = true;
iterateAndNextNull(nodep);
}
void visit(AstCoverpoint* nodep) override {
scanSampleExpression(nodep->exprp());
scanSampleExpression(nodep->iffp());
iterateAndNextNull(nodep->binsp());
iterateAndNextNull(nodep->optionsp());
}
void visit(AstCoverCross* nodep) override {
iterateAndNextNull(nodep->itemsp());
scanSampleExpression(nodep->iffp());
iterateAndNextNull(nodep->optionsp());
iterateAndNextNull(nodep->binsp());
}
void visit(AstCoverCrossBin* nodep) override {
iterateAndNextNull(nodep->selectp());
scanSampleExpression(nodep->iffp());
}
void visit(AstCoverBinsof* nodep) override { scanCoverageExpression(nodep->rangesp()); }
void visit(AstCoverBin* nodep) override {
scanCoverageExpression(nodep->rangesp());
scanSampleExpression(nodep->iffp());
scanCoverageExpression(nodep->arraySizep());
scanCoverageExpression(nodep->transp());
}
void visit(AstVarRef* nodep) override {
if (!m_sampleFormalAllowed && m_sampleMembers.count(nodep->varp())) {
nodep->v3error("Covergroup sample formal argument "
<< nodep->varp()->prettyNameQ()
<< " may only be used in a coverpoint or conditional guard "
"expression (IEEE 1800-2023 19.8.1).");
}
if (m_inCoverageExpression && m_constructorRefMembers.count(nodep->varp())) {
nodep->v3error("Ref covergroup constructor formal argument "
<< nodep->varp()->prettyNameQ()
<< " may not be used in a covergroup expression "
"(IEEE 1800-2023 19.5).");
}
}
void visit(AstNodeFTaskRef* nodep) override {
if (m_inCoverageExpression && nodep->taskp()) {
bool invalidDirection = false;
for (AstNode* stmtp = nodep->taskp()->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
const AstVar* const varp = VN_CAST(stmtp, Var);
if (varp && varp->isIO() && varp->isWritable()) {
invalidDirection = true;
break;
}
}
if (invalidDirection) {
nodep->v3error("Function " << nodep->taskp()->prettyNameQ()
<< " called in a covergroup expression has an "
"output, inout, or non-const ref argument "
"(IEEE 1800-2023 19.5).");
}
}
iterateChildren(nodep);
}
void visit(AstNode* nodep) override { iterateChildren(nodep); }
public:
CovergroupExprValidVisitor(const std::set<const AstVar*>& sampleMembers,
const std::set<const AstVar*>& constructorRefMembers)
: m_sampleMembers{sampleMembers}
, m_constructorRefMembers{constructorRefMembers} {}
void scan(AstNode* nodep) { iterate(nodep); }
};
//######################################################################
// Bins of one declaration whose values are computed rather than listed: bin k covers
// [m_lo + k * m_stride, m_lo + (k + 1) * m_stride - 1], and the last bin extends to m_hi. An
// array bin element is a run of single-value bins; automatic bins partition the coverpoint
// domain. Bounds are coverpoint values at FunctionalCoverageVisitor::runWidth(),
// sign-extended like a CrossValueRange's.
class BinRun final {
public:
// MEMBERS
uint32_t m_count; // Number of bins
V3Number m_lo; // Lowest value of the first bin
V3Number m_stride; // Number of values of each bin but the last
V3Number m_hi; // Highest value of the last bin
bool m_empty = false; // A single bin without a value of the coverpoint type
uint32_t m_declared = 0; // Runtime index of the first bin, once generated
// CONSTRUCTORS
BinRun(AstNode* nodep, int width, uint32_t count)
: m_count{count}
, m_lo{nodep, width}
, m_stride{nodep, width, 1}
, m_hi{nodep, width} {}
};
//######################################################################
// Functional coverage visitor
class FunctionalCoverageVisitor final : public VNVisitor {
// NODE STATE
// Entire netlist:
// AstCoverpoint::user1p() -> AstVar*. Previous-value variable for transition bins
// AstCoverpoint::user2() -> bool. Had a bins declaration ignored, so no automatic bins
const VNUser1InUse m_inuser1;
const VNUser2InUse m_inuser2;
// STATE
std::set<AstCoverpoint*>
m_runtimePoints; // Points needing value metadata and live-bin mapping
std::set<AstCoverCross*> m_runtimeCrosses; // Crosses over finalized live-bin dimensions
std::map<AstVar*, AstVar*> m_excludedVars; // Sample-time state-exclusion flags
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<AstCoverpoint*> m_coverpoints; // Coverpoints in current covergroup
std::map<std::string, AstCoverpoint*> m_coverpointMap; // Name -> coverpoint for fast lookup
std::vector<AstCoverCross*> m_coverCrosses; // Cross coverage items in current covergroup
std::vector<AstCgOptionAssign*> m_cgOptions; // Covergroup-level weights, before lowering
uint32_t m_cgTypeWeight = 1; // The covergroup's type_option.weight, a constant
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<std::string> m_crossedCpNames; // Coverpoints referenced by a cross
std::map<std::string, AstVar*> m_cpVarMap; // Coverpoint name -> its VlCoverpoint member
struct BinRuns final {
std::vector<BinRun> runs; // Runs of an array or automatic bins declaration, in order
uint32_t count = 0; // Bins across all runs
bool unsupported = false; // Too many bins, or invalid: the declaration is ignored
// The value of each bin, which names it, of a wildcard array; else the bins are indexed
std::vector<std::string> values;
// Too many values of an ignore or illegal wildcard array, which is then one bin
bool single = false;
};
struct CrossBinValues final {
AstCoverBin* binp; // Declaration owning this Normal bin
AstNodeExpr* valuep; // Individual array-bin value, or nullptr for a scalar bin
const BinRun* runp = nullptr; // Run computing the bin's values, if any
uint32_t element = 0; // Index of the bin within runp
};
struct BinSpan final {
uint32_t first = 0; // First Normal index of the bin declaration
uint32_t count = 0; // Number of Normal bins of the declaration
uint32_t declared = 0; // First runtime bin index, across all bin kinds
int32_t sized = -1; // Index of the sized array, placed at construction; or -1
};
struct CoverpointBins final {
uint32_t total = 0; // Number of Normal bins
AstNodeExpr* exprp = nullptr; // Sampled expression, for the value domain
bool crossed = false; // Feeds a cross, which needs 'values'
std::vector<CrossBinValues> values; // Values in runtime Normal-bin index order
std::unordered_map<std::string, BinSpan> spans; // Declared bin name -> index span
BinSpan implicitAuto{0, 0, 0}; // Implicit automatic bins, each named 'auto_<i>'
std::deque<BinRun> runs; // Runs 'values' refers to
};
std::map<AstVar*, CoverpointBins> m_cpBins; // Runtime coverpoint -> binsof index ranges
// Names of the bins declarations each coverpoint ignored, which binsof selects as no bins
std::map<const AstCoverpoint*, std::vector<std::string>> m_droppedBins;
// Prefixes of the constructor temporaries of constructed bins, as coverpoint and bin names
// alone may repeat: coverpoint 'a_' bins 'b', and coverpoint 'a' bins '_b'
V3UniqueNames m_sizedNames{"__Vsized"};
std::vector<AstNodeExpr*> m_detachedValues; // Array-bin values m_cpBins refers to
std::set<AstCoverCross*>
m_droppedCrosses; // Crosses with a bare-variable item: drop (COVERIGN)
std::map<uint32_t, AstCoverpointDType*> m_cpDTypes; // Hit-list bound -> interned dtype
using CrossShape = std::tuple<uint32_t, uint32_t, uint32_t, uint32_t, uint64_t, bool>;
std::map<CrossShape, AstCoverCrossDType*> m_cxDTypes;
AstVar* m_cgInstVarp = nullptr; // __Vcg_inst handle member of the current covergroup
VMemberMap m_memberMap; // Member names cached for fast lookup
// METHODS
// The covergroup's 'option' or static 'type_option' member (V3LinkParse creates both)
AstVar* optionVar(bool typeOption) {
AstVar* const varp = VN_AS(
m_memberMap.findMember(m_covergroupp, typeOption ? "type_option" : "option"), Var);
UASSERT_OBJ(varp, m_covergroupp, "Covergroup missing option member");
return varp;
}
// 'option.weight' or 'type_option.weight', per optionVarp
AstStructSel* newWeightSel(FileLine* fl, AstVar* optionVarp, VAccess access) {
const AstMemberDType* const memberp = VN_AS(
m_memberMap.findMember(optionVarp->dtypep()->skipRefp(), "weight"), MemberDType);
UASSERT_OBJ(memberp, optionVarp, "Coverage option structure missing 'weight'");
AstNodeExpr* const fromp = optionVarp->lifetime().isStatic()
? new AstVarRef{fl, optionVarp, access}
: memberRef(fl, optionVarp, access);
AstStructSel* const selp = new AstStructSel{fl, fromp, "weight"};
selp->dtypep(memberp->subDTypep()->skipRefToEnump());
selp->didWidth(true);
return selp;
}
// Store the covergroup-level weights (IEEE 1800-2023 19.7) where SystemVerilog and the
// runtime read them. option.weight is evaluated by the constructor, as are the other
// instance options; type_option.weight is constant, and initializes the static member.
void lowerCovergroupOptions() {
for (AstCgOptionAssign* const optp : m_cgOptions) {
UASSERT_OBJ(optp->optType() == VCoverOptionType::WEIGHT, optp,
"Unexpected covergroup option reaching V3Covergroup");
FileLine* const fl = optp->fileline();
// V3Width left type_option.weight a non-negative constant
if (optp->typeOption()) m_cgTypeWeight = VN_AS(optp->valuep(), Const)->toUInt();
AstAssign* const assignp = new AstAssign{
fl, newWeightSel(fl, optionVar(optp->typeOption()), VAccess::WRITE),
optp->valuep()->unlinkFrBack()};
if (optp->typeOption()) {
m_covergroupp->addMembersp(new AstInitialStatic{fl, assignp});
VL_DO_DANGLING(pushDeletep(optp->unlinkFrBack()), optp);
} else {
optp->replaceWith(assignp);
VL_DO_DANGLING(pushDeletep(optp), optp);
}
}
m_cgOptions.clear();
}
// The weight of an item in the coverage database, which merges the instances: its
// option.weight if a constant, and so of every instance; else its type_option.weight, the
// weight of type coverage merged over the instances (IEEE 1800-2023 19.7.1)
static uint32_t itemDatabaseWeight(AstNode* optionsp) {
const AstNodeExpr* weightp = nullptr; // The option.weight in effect
uint32_t typeWeight = 1;
for (AstNode* nodep = optionsp; nodep; nodep = nodep->nextp()) {
const AstCoverOption* const optp = VN_AS(nodep, CoverOption);
if (!(optp->optType() == VCoverOptionType::WEIGHT)) continue;
// V3Width left type_option.weight a non-negative constant
if (optp->typeOption()) {
typeWeight = VN_AS(optp->valuep(), Const)->toUInt();
} else {
weightp = optp->valuep();
}
}
if (!weightp) return 1;
if (const AstConst* const constp = VN_CAST(weightp, Const)) return constp->toUInt();
return typeWeight;
}
// Configure an item's option.weight, its weight in instance coverage (IEEE 1800-2023
// 19.11). type_option.weight only weighs type coverage merged over the instances, which
// type_option.merge_instances would select; without that, it has no effect.
void generateItemWeight(FileLine* fl, AstVar* itemVarp, AstNode* optionsp) {
for (AstNode* nodep = optionsp; nodep; nodep = nodep->nextp()) {
const AstCoverOption* const optp = VN_AS(nodep, CoverOption);
if (!(optp->optType() == VCoverOptionType::WEIGHT) || optp->typeOption()) continue;
m_constructorp->addStmtsp(
itemCall(fl, itemVarp, VCMethod::COVERGROUP_WEIGHT,
{optp->valuep()->cloneTree(false), fileLineDebug(optp->fileline())})
->makeStmt());
}
}
void processCovergroup() {
UINFO(4, "Processing covergroup: " << m_covergroupp->name() << " with "
<< m_coverpoints.size() << " coverpoints and "
<< m_coverCrosses.size() << " crosses");
m_crossedCpNames.clear();
m_cpVarMap.clear();
m_cpBins.clear();
m_droppedBins.clear();
m_sizedNames.reset();
m_runtimePoints.clear();
m_runtimeCrosses.clear();
m_excludedVars.clear();
m_droppedCrosses.clear();
m_cgInstVarp = nullptr;
m_cgTypeWeight = 1;
lowerCovergroupOptions();
// 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());
}
}
}
std::vector<AstNode*> pending;
std::map<AstCoverpoint*, std::vector<AstCoverCross*>> consumers;
std::map<AstCoverCross*, std::vector<AstCoverpoint*>> inputs;
for (AstCoverpoint* const cpp : m_coverpoints) {
checkBinNames(cpp);
checkConstructedBins(cpp);
if (!cpp->exprp()->dtypep()->skipRefp()->isIntegralOrPacked()) continue;
// Bins without values leave the report (IEEE 1800-2023 19.11.1), exclusions or not.
// Constructed bins get their values when the covergroup is constructed.
if (!coverpointHasStateExclusions(cpp) && !coverpointHasEmptyBins(cpp)
&& !coverpointHasConstructedBins(cpp)) {
continue;
}
m_runtimePoints.insert(cpp);
pending.push_back(cpp);
}
for (AstCoverCross* const crossp : m_coverCrosses) {
if (m_droppedCrosses.count(crossp)) continue;
for (AstNode* itemp = crossp->itemsp(); itemp; itemp = itemp->nextp()) {
const AstCoverpointRef* const refp = VN_AS(itemp, CoverpointRef);
if (refp->exprp()) continue;
const auto point = m_coverpointMap.find(refp->name());
if (point != m_coverpointMap.end()) {
consumers[point->second].push_back(crossp);
inputs[crossp].push_back(point->second);
}
}
}
for (size_t next = 0; next < pending.size(); ++next) {
if (AstCoverpoint* const pointp = VN_CAST(pending[next], Coverpoint)) {
for (AstCoverCross* const crossp : consumers[pointp]) {
if (m_runtimeCrosses.emplace(crossp).second) pending.push_back(crossp);
}
} else {
for (AstCoverpoint* const pointp : inputs[VN_AS(pending[next], CoverCross)]) {
if (pointp->exprp()->dtypep()->skipRefp()->isIntegralOrPacked()
&& m_runtimePoints.emplace(pointp).second) {
pending.push_back(pointp);
}
}
}
}
// The instance node owns this instance's coverpoint/cross runtimes, so it must exist
// before any of them is created. Emitted first, ahead of both generate loops.
generateInstanceAttach();
// 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);
// Every cross has been built, so runtime points only need their exclusions from here.
for (AstCoverpoint* const cpp : m_coverpoints) {
if (!m_runtimePoints.count(cpp)) continue;
m_constructorp->addStmtsp(itemCall(cpp->fileline(), m_cpVarMap.at(cpp->name()),
VCMethod::COVERGROUP_VALUE_RELEASE)
->makeStmt());
}
for (AstNodeExpr* valuep : m_detachedValues) VL_DO_DANGLING(pushDeletep(valuep), valuep);
m_detachedValues.clear();
// 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).
generateCoverageComputationCode();
}
static constexpr size_t VALUE_LIST_ENTRIES = 256; // Metadata entries per constructor call
// The number of bins a constant array size requests: -1 if it is negative, and saturated
// above the largest limit
static int64_t binsCount(const AstConst* constp) {
const V3Number& num = constp->num();
if (constp->isSigned() && num.isNegative()) return -1;
return num.mostSetBitP1() > 32 ? INT64_MAX : static_cast<int64_t>(num.toUQuad());
}
// The number of bins requested by a valid 'bins auto[N]', or 0
static uint32_t autoBinsRequested(const AstCoverBin* binp) {
const AstConst* const constp = VN_CAST(binp->arraySizep(), Const);
if (!constp) return 0;
const int64_t count = binsCount(constp);
return count < 1 || count > v3Global.opt.coverageMaxBins() ? 0
: static_cast<uint32_t>(count);
}
// True for a 'bins auto[N]' declaration, or the implicit automatic bins of a coverpoint
static bool isAutoBins(const AstCoverBin* binp) {
return binp->binsType() == VCoverBinsType::BINS_AUTO
|| binp->binsType() == VCoverBinsType::BINS_AUTO_IMPLICIT;
}
// True for a sized array of bins, 'bins b[N] = {...}', whose values an integral coverpoint
// distributes over N bins when the covergroup is constructed (IEEE 1800-2023 19.5.1)
static bool isSizedArray(const AstCoverBin* binp) {
return binp->arraySizep() && !isAutoBins(binp);
}
// The 'with' filter of a bin, if any (IEEE 1800-2023 19.5.1.1)
static AstCoverWith* binWith(const AstCoverBin* binp) {
return VN_CAST(binp->rangesp(), CoverWith);
}
// True for bins whose values the covergroup constructor computes: a sized array, or bins of
// the values a 'with' filter keeps
static bool isConstructedBins(const AstCoverBin* binp) {
return isSizedArray(binp) || binWith(binp);
}
// The range list of a bin's values, or of the candidates of its 'with' filter; null for all
// of the coverpoint's values, which a filter of the coverpoint's name has
static AstNode* binRangesp(const AstCoverBin* binp) {
const AstCoverWith* const withp = binWith(binp);
if (!withp) return binp->rangesp();
return VN_IS(withp->subp(), CoverpointRef) ? nullptr : withp->subp();
}
// Report and delete a bins declaration of a name that another of its coverpoint has
void checkBinNames(AstCoverpoint* coverpointp) {
std::set<std::string> names;
for (AstNode* nodep = coverpointp->binsp(); nodep;) {
AstCoverBin* const binp = VN_AS(nodep, CoverBin);
nodep = nodep->nextp();
if (names.emplace(binp->name()).second) continue;
binp->v3error("Duplicate bin " << binp->prettyNameQ() << " in coverpoint "
<< coverpointp->prettyNameQ()
<< " (IEEE 1800-2023 3.13)");
VL_DO_DANGLING(pushDeletep(binp->unlinkFrBack()), binp);
}
}
// Delete an ignored bins declaration, which binsof then selects as no bins
void dropBins(const AstCoverpoint* coverpointp, AstCoverBin* binp) {
m_droppedBins[coverpointp].push_back(binp->name());
VL_DO_DANGLING(pushDeletep(binp->unlinkFrBack()), binp);
}
// Check the size of a sized array of bins, which drops an invalid array. A real coverpoint's
// are unsupported, and treated as arrays of a bin per value. False unless it stays sized.
bool checkBinsArraySize(const AstCoverpoint* coverpointp, AstCoverBin* binp, bool integral) {
AstNodeExpr* const sizep = binp->arraySizep();
const AstConst* const constp = VN_CAST(sizep, Const);
if (VN_IS(sizep, Unbounded)) { // A parameter of '$'; see bins_orBraE
binp->v3error("Bins array size must be integral, not '$' (IEEE 1800-2023 19.5.1)");
} else if (!sizep->dtypep()->skipRefp()->isIntegralOrPacked()) {
sizep->v3error("Bins array size must be integral (IEEE 1800-2023 19.5.1)");
} else if (constp && (constp->num().isFourState() || binsCount(constp) < 1)) {
sizep->v3error("Bins array size must be >= 1, got "
<< (constp->num().isFourState() ? constp->num().ascii(false)
: constp->num().toDecimalS())
<< " (IEEE 1800-2023 19.5.1)");
} else if (!integral) {
binp->v3warn(COVERIGN, "Unsupported: 'bins' explicit array size of a real "
"coverpoint (treated as '[]')");
VL_DO_DANGLING(pushDeletep(sizep->unlinkFrBack()), sizep);
return false;
} else {
return true;
}
dropBins(coverpointp, binp);
return false;
}
// Check the bins of a coverpoint whose values the constructor computes, dropping invalid
// ones. A wildcard array of too many ranges of values is ignored, or if ignore or illegal,
// treated as one bin; filtering too many ranges of values ignores the bins.
void checkConstructedBins(AstCoverpoint* coverpointp) {
const bool integral = coverpointp->exprp()->dtypep()->skipRefp()->isIntegralOrPacked();
for (AstNode* nodep = coverpointp->binsp(); nodep;) {
AstCoverBin* const binp = VN_AS(nodep, CoverBin);
nodep = nodep->nextp();
if (!isConstructedBins(binp)) continue;
if (isSizedArray(binp) && !checkBinsArraySize(coverpointp, binp, integral)) continue;
if (!binp->isWildcard()
|| sizedWildcardRuns(binp, coverpointp->exprp())
<= v3Global.opt.coverageMaxBins()) {
if (binWith(binp) && withCandidatesOver(binp, coverpointp->exprp())) {
binp->v3warn(COVERIGN, "Unsupported: 'with' filter of more than 2**32 "
"candidate values; bin "
<< binp->prettyNameQ() << " ignored");
if (binp->binsType().binIsNormal()) coverpointp->user2(true);
dropBins(coverpointp, binp);
}
continue;
}
// An ignore or illegal array still excludes or checks its values, as one bin
const bool single = !binp->binsType().binIsNormal() && !binWith(binp);
binp->v3warn(COVERIGN,
"Unsupported: " << (binWith(binp) ? "'with' filter of wildcard '"
: "sized wildcard array '")
<< binp->binsType().verilogKwd()
<< "' of more than --coverage-max-bins of "
<< v3Global.opt.coverageMaxBins()
<< " ranges of values; bin " << binp->prettyNameQ()
<< (single ? " treated as one bin" : " ignored") << "\n"
<< binp->warnMore()
<< "... Suggest a larger --coverage-max-bins");
if (single) {
AstNodeExpr* const sizep = binp->arraySizep();
VL_DO_DANGLING(pushDeletep(sizep->unlinkFrBack()), sizep);
binp->isArray(false);
continue;
}
if (binp->binsType().binIsNormal()) coverpointp->user2(true);
dropBins(coverpointp, binp);
}
}
// True if a 'with' filter would be evaluated for more than 2**32 candidate values, known
// now for the coverpoint's name, or for a range list of constants: each is evaluated once
// but for an array 'b[N]', which keeps their order and duplicates (see withBegin())
static bool withCandidatesOver(AstCoverBin* binp, AstNodeExpr* exprp) {
const int width = runWidth(exprp);
std::vector<std::pair<V3Number, V3Number>> runs;
if (!binRangesp(binp)) runs = coverpointValues(binp, exprp);
const auto constant
= [](const AstNode* nodep) { return VN_IS(nodep, Const) || VN_IS(nodep, Unbounded); };
for (AstNode* rangep = binRangesp(binp); rangep; rangep = rangep->nextp()) {
const AstInsideRange* const irp = VN_CAST(rangep, InsideRange);
// Else known when constructed
if (irp ? !constant(irp->lhsp()) || !constant(irp->rhsp()) : !VN_IS(rangep, Const)) {
return false;
}
CrossValueRange range{rangep, resolveWidth(rangep, exprp)};
if (!resolveValue(rangep, exprp, true, binp->isWildcard(), range)
|| crossRangeEmpty(range)) {
continue;
}
std::vector<std::pair<V3Number, V3Number>> found{{range.lo, range.hi}};
if (range.wildcard) { // checkConstructedBins bounded the runs
found.clear();
crossRangeRuns(range, v3Global.opt.coverageMaxBins(), found);
}
for (const std::pair<V3Number, V3Number>& run : found) {
runs.emplace_back(V3Number{rangep, width, run.first},
V3Number{rangep, width, run.second});
}
}
if (!isSizedArray(binp)) { // The union of the values
std::sort(runs.begin(), runs.end(), [](const auto& lhs, const auto& rhs) {
return crossValueLess(lhs.first, rhs.first);
});
std::vector<std::pair<V3Number, V3Number>> merged;
for (const std::pair<V3Number, V3Number>& run : runs) {
if (merged.empty() || crossValueLess(merged.back().second, run.first)) {
merged.push_back(run);
} else if (crossValueLess(merged.back().second, run.second)) {
merged.back().second = run.second;
}
}
runs = std::move(merged);
}
uint64_t count = 0;
for (const std::pair<V3Number, V3Number>& run : runs) {
V3Number span{exprp, width};
span.opSub(run.second, run.first);
if (span.mostSetBitP1() > 32) return true; // More than 2**32 values
count += span.toUQuad() + 1;
if (count > (uint64_t{1} << 32)) return true;
}
return false;
}
// The ranges of values the wildcard patterns of a sized wildcard array, or of a 'with'
// filter's candidates, give, counted up to more than --coverage-max-bins
static size_t sizedWildcardRuns(const AstCoverBin* binp, AstNodeExpr* exprp) {
std::vector<std::pair<V3Number, V3Number>> runs;
for (AstNode* rangep = binRangesp(binp); rangep; rangep = rangep->nextp()) {
if (!VN_IS(rangep, Const)) continue; // A range, or a value known at construction
CrossValueRange range{rangep, resolveWidth(rangep, exprp)};
if (resolveValue(rangep, exprp, true, true, range) && !crossRangeEmpty(range)) {
crossRangeRuns(range, v3Global.opt.coverageMaxBins(), runs);
}
}
return runs.size();
}
// Check the automatic bins declarations of a coverpoint. Each stays one declaration, which
// generates as a partition of the coverpoint domain (see autoBinRuns).
void checkAutomaticBins(AstCoverpoint* coverpointp, const AstNodeExpr* exprp) {
for (AstNode* binp = coverpointp->binsp(); binp; binp = binp->nextp()) {
AstCoverBin* const cbinp = VN_AS(binp, CoverBin);
if (cbinp->binsType() != VCoverBinsType::BINS_AUTO) continue;
const AstConst* const constp = VN_CAST(cbinp->arraySizep(), Const);
if (!constp) {
cbinp->v3error("Automatic bins array size must be a constant");
} else if (binsCount(constp) < 1) {
cbinp->v3error("Automatic bins array size must be >= 1, got "
<< constp->num().toDecimalS());
} else if (binsCount(constp) > v3Global.opt.coverageMaxBins()) {
cbinp->v3error("Automatic bins array size of "
<< constp->num().toDecimalU() << " exceeds limit of "
<< v3Global.opt.coverageMaxBins() << '\n'
<< cbinp->warnMore() << "... Suggest a larger --coverage-max-bins");
} else if (!exprp->dtypep()->skipRefp()->isIntegralOrPacked()) {
cbinp->v3error("Automatic bins are not allowed on a coverpoint of a non-integral "
"expression (IEEE 1800-2023 19.5.3).");
}
}
}
// 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);
// Weights may be non-constant; generateItemWeight() handles them
if (optp->optType() == VCoverOptionType::WEIGHT) continue;
AstConst* const constp = VN_CAST(optp->valuep(), Const);
if (!constp) {
optp->valuep()->v3warn(COVERIGN, "Ignoring unsupported: non-constant 'option."
<< optp->optType().ascii()
<< "'; using default value");
continue;
}
if (optp->optType() == VCoverOptionType::AT_LEAST) {
atLeastOut = constp->toSInt();
} else {
// V3LinkParse only converts at_least/auto_bin_max/weight coverpoint options
// into AstCoverOption (others are dropped there), so this is the only
// alternative.
UASSERT_OBJ(optp->optType() == 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
}
}
}
// IEEE 1800-2023 19.5.2: an enum coverpoint has one automatic bin per enumeration value
void createEnumAutoBins(AstCoverpoint* coverpointp, AstNodeExpr* exprp,
const AstEnumDType* enump) {
FileLine* const fl = coverpointp->fileline();
for (const AstEnumItem* itemp = enump->itemsp(); itemp;
itemp = VN_AS(itemp->nextp(), EnumItem)) {
AstConst* const lop = newValueConst(fl, VN_AS(itemp->valuep(), Const)->num(), exprp);
AstInsideRange* const rangep = new AstInsideRange{fl, lop, lop->cloneTree(false)};
rangep->dtypeFrom(exprp);
coverpointp->addBinsp(
new AstCoverBin{fl, "auto[" + itemp->name() + "]", rangep, false, false});
}
}
// IEEE 1800-2023 19.5.3/19.11.1: partition first, then apply exclusions. The partition is one
// automatic bins declaration, generated as a run like 'bins auto[N]' but numbering its bins.
void createImplicitAutoBins(AstCoverpoint* coverpointp, AstNodeExpr* exprp, int autoBinMax) {
if (coverpointp->user2()) return; // Declared bins, ignored, leave no bins
for (AstNode* nodep = coverpointp->binsp(); nodep; nodep = nodep->nextp()) {
const VCoverBinsType kind = VN_AS(nodep, CoverBin)->binsType();
if (kind != VCoverBinsType::BINS_IGNORE && kind != VCoverBinsType::BINS_ILLEGAL)
return;
}
if (const AstEnumDType* const enump
= VN_CAST(exprp->dtypep()->skipRefToEnump(), EnumDType)) {
createEnumAutoBins(coverpointp, exprp, enump);
return;
}
const int width = exprp->width();
uint32_t count = width < 31 ? std::min<uint32_t>(uint32_t{1} << width, autoBinMax)
: static_cast<uint32_t>(autoBinMax);
if (!count) return;
if (!exprp->dtypep()->skipRefp()->isIntegralOrPacked()) {
coverpointp->v3error("Coverpoint of a non-integral expression requires explicit bins "
"(IEEE 1800-2023 19.5.3).");
return;
}
if (count > v3Global.opt.coverageMaxBins()) {
coverpointp->v3warn(COVERIGN, "Unsupported: more than "
<< v3Global.opt.coverageMaxBins()
<< " automatic bins from 'option.auto_bin_max'; "
"using "
<< v3Global.opt.coverageMaxBins() << ".\n"
<< coverpointp->warnMore()
<< "... Suggest a larger --coverage-max-bins");
count = v3Global.opt.coverageMaxBins();
}
FileLine* const fl = coverpointp->fileline();
coverpointp->addBinsp(new AstCoverBin{fl, "auto", new AstConst{fl, count},
VCoverBinsType::BINS_AUTO_IMPLICIT});
}
// 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()};
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};
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* exprp = coverpointp->exprp();
// Check automatic bins before processing
checkAutomaticBins(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);
AstVar* const valueVarp = new AstVar{
coverpointp->fileline(), VVarType::BLOCKTEMP,
"__VcpValue_" + sanitizeGeneratedName(coverpointp->name()), exprp->dtypep()};
valueVarp->funcLocal(true);
m_sampleFuncp->addStmtsp(valueVarp);
exprp->unlinkFrBack();
m_sampleFuncp->addStmtsp(new AstAssign{
coverpointp->fileline(),
new AstVarRef{coverpointp->fileline(), valueVarp, VAccess::WRITE}, exprp});
coverpointp->exprp(new AstVarRef{coverpointp->fileline(), valueVarp, VAccess::READ});
exprp = coverpointp->exprp();
// 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;
// The values of constructed bins are known at construction; see emitSizedSample
if (isConstructedBins(cbinp)) continue;
if (isAutoBins(cbinp)) {
// Automatic bins partition the whole domain, leaving no default value
if (anyBinMatchp) VL_DO_DANGLING(pushDeletep(anyBinMatchp), anyBinMatchp);
return new AstConst{fl, AstConst::BitFalse{}};
}
AstNodeExpr* const binCondp = buildBinCondition(cbinp, exprp);
UASSERT_OBJ(binCondp, cbinp,
"buildBinCondition returned nullptr for non-ignore/non-illegal bin");
anyBinMatchp = anyBinMatchp ? new AstOr{fl, anyBinMatchp, binCondp} : binCondp;
}
return anyBinMatchp ? static_cast<AstNodeExpr*>(new AstNot{fl, anyBinMatchp})
: static_cast<AstNodeExpr*>(new AstConst{fl, AstConst::BitTrue{}});
}
//====================================================================
// VlCoverpoint conversion
static bool coverpointHasStateExclusions(const AstCoverpoint* coverpointp) {
for (const AstNode* nodep = coverpointp->binsp(); nodep; nodep = nodep->nextp()) {
const AstCoverBin* const binp = VN_AS(nodep, CoverBin);
if (!binp->transp() && binp->rangesp()
&& (binp->binsType() == VCoverBinsType::BINS_IGNORE
|| binp->binsType() == VCoverBinsType::BINS_ILLEGAL)) {
return true;
}
}
return false;
}
static bool coverpointHasConstructedBins(const AstCoverpoint* coverpointp) {
for (const AstNode* nodep = coverpointp->binsp(); nodep; nodep = nodep->nextp()) {
if (isConstructedBins(VN_AS(nodep, CoverBin))) return true;
}
return false;
}
// True if a Normal state bin, or an array-bin element, has no value of the coverpoint's
// type (IEEE 1800-2023 19.5.7). Array ranges enumerate in-type values, so cannot vanish.
static bool coverpointHasEmptyBins(const AstCoverpoint* coverpointp) {
AstNodeExpr* const exprp = coverpointp->exprp();
for (AstNode* nodep = coverpointp->binsp(); nodep; nodep = nodep->nextp()) {
const AstCoverBin* const binp = VN_AS(nodep, CoverBin);
if (!binp->binsType().binIsNormal() || binp->transp() || !binp->rangesp()) continue;
// A wildcard array has a bin for each value its elements match, so none empty, and
// the constructor creates no bin without values of a 'with' filter
if ((binp->isArray() && binp->isWildcard()) || binWith(binp)) continue;
bool empty = true;
for (AstNode* valuep = binp->rangesp(); valuep; valuep = valuep->nextp()) {
if (binp->isArray() && VN_IS(valuep, InsideRange)) continue;
CrossValueRange range{valuep, resolveWidth(valuep, exprp)};
const bool none = resolveValue(valuep, exprp, true, binp->isWildcard(), range)
&& crossRangeEmpty(range);
if (binp->isArray() && none) return true;
empty &= none;
}
if (empty && !binp->isArray()) return true;
}
return false;
}
// 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;
}
// The interned AstBasicDType for one of the covergroup runtime keywords.
static AstBasicDType* basicDType(FileLine* fl, VBasicDTypeKwd kwd) {
return v3Global.rootp()->typeTablep()->findBasicDType(fl, kwd);
}
// Get (or create) the coverpoint dtype for a hit-list bound, interned so each distinct bound
// yields one node.
AstCoverpointDType* coverpointDType(FileLine* fl, uint32_t hitBound) {
AstCoverpointDType*& typep = m_cpDTypes[hitBound];
if (!typep) {
typep = new AstCoverpointDType{fl, hitBound};
v3Global.rootp()->typeTablep()->addTypesp(typep);
}
return typep;
}
std::string covergroupProtectedName() const {
return VIdProtect::protectWordsIf(m_covergroupp->name(), v3Global.opt.protectIds());
}
// Emit the covergroup's instance handle member and the constructor statement that creates
// its node in the per-context coverage registry. Runs before any coverpoint or cross is
// generated, so their runtimes can be added to the node as they are created.
void generateInstanceAttach() {
FileLine* const fl = m_covergroupp->fileline();
// V3LinkParse synthesizes a 'new' for every covergroup; the item generators below already
// rely on that, and this attach runs even for a covergroup with no coverpoints at all.
UASSERT_OBJ(m_constructorp, m_covergroupp, "Covergroup missing synthesized constructor");
m_cgInstVarp = new AstVar{fl, VVarType::MEMBER, "__Vcg_inst",
basicDType(fl, VBasicDTypeKwd::COVERGROUP_INSTHANDLE)};
m_covergroupp->addMembersp(m_cgInstVarp);
m_constructorp->addStmtsp(
itemCall(fl, m_cgInstVarp, VCMethod::COVERGROUP_ATTACH,
{ctext(fl, "vlSymsp->_vm_contextp__->covergroupRegistryp()"
"->newCovergroupInst("
+ quoted(covergroupProtectedName()) + ")")},
/*usePtr=*/false)
->makeStmt());
// The node reads option.weight in place, so procedural assignments take effect
AstCExpr* const weightAddrp = new AstCExpr{fl, "&"};
weightAddrp->add(newWeightSel(fl, optionVar(false), VAccess::READ));
m_constructorp->addStmtsp(itemCall(fl, m_cgInstVarp, VCMethod::COVERGROUP_LEND_WEIGHT,
{weightAddrp, fileLineDebug(fl)}, /*usePtr=*/false)
->makeStmt());
}
// A '__Vcg_inst.p()-><method>()' call on the covergroup's instance node
AstCMethodHard* instanceCall(FileLine* fl, VCMethod method) {
// '__Vcg_inst.p()' -- a value handle, so '.' not '->'
AstCMethodHard* const instp
= new AstCMethodHard{fl, memberRef(fl, m_cgInstVarp), VCMethod::COVERGROUP_INST_P};
instp->usePtr(false);
instp->dtypeSetVoid(); // Opaque receiver; only ever the 'fromp' of the call below
AstCMethodHard* const callp = new AstCMethodHard{fl, instp, method};
callp->usePtr(true);
return callp;
}
// Emit 'this->__Vcp_x = this->__Vcg_inst.p()->addCoverpoint<K>();' (or addCross), which
// creates the item runtime in the instance node and borrows a pointer to it.
AstAssign* makeItemCreate(FileLine* fl, AstVar* itemVarp, VCMethod method) {
AstCMethodHard* const createp = instanceCall(fl, method);
createp->dtypep(itemVarp->dtypep());
return new AstAssign{fl, memberRef(fl, itemVarp, VAccess::WRITE), createp};
}
// 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<std::pair<uint64_t, uint64_t>>& out) {
if (!cbinp->rangesp()) return false;
for (AstNode* rp = cbinp->rangesp(); rp; rp = rp->nextp()) {
RangeBounds rb;
if (!constRangeBounds(rp, rb)) return false;
if ((rb.loConstp() && rb.loConstp()->width() > 64)
|| (rb.hiConstp() && rb.hiConstp()->width() > 64)) {
return false; // Use the safe slot-count bound for wide values.
}
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 an upper
// bound of the bin's slots holding one value. Returns false if any part isn't statically
// enumerable (the caller then falls back to the always-safe slot count). A non-array bin
// is one slot covering the union of its intervals; the bins of an array element or of an
// automatic bins declaration hold disjoint values, so the element or declaration counts once.
bool appendBinCrossSlots(AstCoverBin* cbinp, uint64_t maxVal, AstNodeExpr* exprp,
std::vector<std::vector<std::pair<uint64_t, uint64_t>>>& bins,
int& slotCount) {
if (isAutoBins(cbinp)) {
++slotCount;
bins.push_back({{0, maxVal}});
return exprp->width() <= 64;
}
if (binWith(cbinp)) {
// A value is in one bin of a filter's, but of a sized array, in one for each range
// list element holding it; the coverpoint's name is one element
int elements = 0;
for (const AstNode* rp = binRangesp(cbinp); rp && isSizedArray(cbinp);
rp = rp->nextp()) {
++elements;
}
slotCount += std::max(1, elements);
return false;
}
if (cbinp->isArray() && cbinp->isWildcard() && !cbinp->arraySizep()) {
// A value is in at most one bin of a wildcard array: one slot covering its values.
// Signed values are sign-extended, not unsigned intervals (see computeHitListBound)
++slotCount;
if (exprp->isSigned() || exprp->width() > 64) return false;
const BinRuns runs = wildcardBinRuns(cbinp, exprp, false);
if (runs.unsupported) return false;
std::vector<std::pair<uint64_t, uint64_t>> ivs;
for (const BinRun& run : runs.runs) {
ivs.emplace_back(run.m_lo.toUQuad(), run.m_hi.toUQuad());
}
bins.push_back(std::move(ivs));
return true;
}
if (cbinp->isArray()) return appendArrayBinCrossSlots(cbinp, exprp, bins, slotCount);
// Non-array bin: one slot covering the union of its intervals.
++slotCount;
std::vector<std::pair<uint64_t, uint64_t>> 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 is a slot covering its
// values, as at most one of its single-value bins holds a value. An element holds the
// values arrayBinRuns() gives it: those of the coverpoint type (IEEE 1800-2023 19.5.7).
// Elements of a signed coverpoint, non-constant elements, and elements with values beyond
// 64 bits 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, AstNodeExpr* exprp,
std::vector<std::vector<std::pair<uint64_t, uint64_t>>>& bins,
int& slotCount) {
// Signed values resolve sign-extended, not as unsigned intervals, and wildcard patterns
// are not intervals
bool exact = !exprp->isSigned() && !cbinp->isWildcard();
for (AstNode* rp = cbinp->rangesp(); rp; rp = rp->nextp()) {
++slotCount;
RangeBounds rb;
CrossValueRange range{rp, resolveWidth(rp, exprp)};
if (!exact || !constRangeBounds(rp, rb)
|| !resolveValue(rp, exprp, true, false, range)) {
exact = false;
continue;
}
if (crossRangeEmpty(range)) continue; // Its bin, if any, holds no value
if (range.hi.mostSetBitP1() > 64) {
exact = false;
continue;
}
bins.push_back({{range.lo.toUQuad(), range.hi.toUQuad()}});
}
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<std::vector<std::pair<uint64_t, uint64_t>>> bins;
int slotCount = 0; // == runtime m_normal; the safe fallback bound
// Unsigned intervals cannot establish overlap between differently sized signed values.
bool exact = !exprp->isSigned();
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, exprp, 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<uint64_t> 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, VAccess access = VAccess::READ) {
AstVarRef* const refp = new AstVarRef{fl, varp, access};
refp->selfPointer(VSelfPointerText{VSelfPointerText::This{}});
return refp;
}
// A 'this->m_member-><method>(args...)' call on one member. usePtr is false only for
// __Vcg_inst, which is a value handle; the item members are borrowed pointers into the
// instance node. Numeric arguments are AstConst; the rest are C++ text that has no AST
// form (see ctext).
AstCMethodHard* itemCall(FileLine* fl, AstVar* varp, VCMethod method,
const std::vector<AstNodeExpr*>& args = {}, bool usePtr = true) {
AstCMethodHard* const callp = new AstCMethodHard{fl, memberRef(fl, varp), method};
for (AstNodeExpr* const argp : args) callp->addPinsp(argp);
callp->usePtr(usePtr);
callp->dtypeSetVoid();
return callp;
}
// An unsigned integer argument.
static AstConst* cnum(FileLine* fl, uint32_t value) { return new AstConst{fl, value}; }
// A literal C++ argument with no AST equivalent: a 'const char*' string literal (an SV
// string AstConst emits '"..."s', a std::string temporary the runtime cannot borrow), a
// VlCovBinKind enum token, a constant selection-word initializer list, a VlFileLineDebug, or
// a '__V' temporary declared by the enclosing AstCStmt.
static AstCExpr* ctext(FileLine* fl, const std::string& text) {
return new AstCExpr{fl, text};
}
// A C++ string literal. Escapes control characters as the emitter does elsewhere -- bin
// names and filenames reach the generated code verbatim when --protect-ids is off, and an
// SV escaped identifier may hold a quote or backslash.
static std::string quoted(const std::string& text) {
return "\"" + V3OutFormatter::quoteNameControls(text) + "\"";
}
// A 'VlFileLineDebug' argument: where the runtime reports an error about fl's construct
static AstCExpr* fileLineDebug(FileLine* fl) {
const std::string filename
= VIdProtect::protectIf(fl->filename(), v3Global.opt.protectIds());
return ctext(fl, "VlFileLineDebug{" + quoted(filename) + ", "
+ std::to_string(fl->lineno()) + "}");
}
// Check that an element of an array bin (bins b[] = {values/ranges}) is a two-state
// constant value or range; false if not, after reporting it if 'report'.
static bool checkArrayBinElement(AstCoverBin* arrayBinp, AstNode* rangep, bool report = true) {
if (const AstInsideRange* const irp = VN_CAST(rangep, InsideRange)) {
const AstConst* const minp = VN_CAST(irp->lhsp(), Const);
const AstConst* const maxp = VN_CAST(irp->rhsp(), Const);
if ((!minp && !VN_IS(irp->lhsp(), Unbounded))
|| (!maxp && !VN_IS(irp->rhsp(), Unbounded))) {
if (report) {
arrayBinp->v3error("Non-constant expression in array bins range; "
"range bounds must be constants (IEEE 1800-2023 19.5)");
}
return false;
}
if ((minp && minp->num().isFourState()) || (maxp && maxp->num().isFourState())) {
if (report) {
arrayBinp->v3error("Four-state (x/z) value in array bins range bound; "
"range bounds must be two-state constants");
}
return false;
}
} else if (!VN_IS(rangep, Const)) {
if (report) {
arrayBinp->v3error("Non-constant expression in array bins value list; "
"values must be constants (IEEE 1800-2023 19.5)");
}
return false;
}
return true;
}
// Individual equality targets of an array bin (bins b[] = {values/ranges}) of a real
// coverpoint, in order; integral coverpoints generate array bins as runs (see arrayBinRuns).
// 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; ranges
// whose resolved size would exceed --coverage-max-real-bins (e.g. an open '[lo:$]') are
// unsupported -- emits COVERIGN, sets unsupportedOut, yields nothing.
std::vector<AstNodeExpr*> 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<AstNodeExpr*> values;
for (AstNode* rangep = arrayBinp->rangesp(); rangep; rangep = rangep->nextp()) {
rangep = V3Const::constifyEdit(rangep);
if (!checkArrayBinElement(arrayBinp, rangep)) return values;
if (AstInsideRange* const irp = VN_CAST(rangep, InsideRange)) {
const bool loUnb = VN_IS(irp->lhsp(), Unbounded);
const bool hiUnb = VN_IS(irp->rhsp(), Unbounded);
const uint64_t lo = loUnb ? 0 : VN_AS(irp->lhsp(), Const)->toUQuad();
const uint64_t hi = hiUnb ? maxVal : VN_AS(irp->rhsp(), Const)->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 >= v3Global.opt.coverageMaxRealBins()
|| values.size() + span + 1 > v3Global.opt.coverageMaxRealBins()) {
arrayBinp->v3warn(COVERIGN,
"Unsupported: array 'bins' of a real coverpoint "
"covering more than "
<< v3Global.opt.coverageMaxRealBins() << " values; bin "
<< arrayBinp->prettyNameQ() << " ignored.\n"
<< arrayBinp->warnMore()
<< "... Suggest a larger --coverage-max-real-bins");
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<uint32_t>(v)});
} else {
values.push_back(VN_AS(rangep->cloneTree(false), NodeExpr));
}
}
return values;
}
static int runWidth(const AstNodeExpr* exprp) { return exprp->width() + 1; }
// Automatic bins partition the coverpoint domain in value order (IEEE 1800-2023 19.5.3): N
// bins, capped at the number of values, each hold 2^width / N values, and the last bin also
// holds the remainder. False for an invalid declaration, already reported.
bool autoBinRuns(AstCoverBin* binp, AstNodeExpr* exprp, BinRuns& out) {
const uint32_t requested = autoBinsRequested(binp);
if (!requested || !exprp->dtypep()->skipRefp()->isIntegralOrPacked()) return false;
const int width = exprp->width();
const int arithmeticWidth = runWidth(exprp);
const uint32_t count
= width < 32
? static_cast<uint32_t>(std::min<uint64_t>(uint64_t{1} << width, requested))
: requested;
BinRun run{binp, arithmeticWidth, count};
V3Number total{binp, arithmeticWidth};
total.setBit(width, 1);
run.m_stride.opDiv(total, V3Number{binp, arithmeticWidth, count});
const CrossValueRange domain
= crossValueDomain(binp, width, exprp->isSigned(), arithmeticWidth);
run.m_lo = domain.lo;
run.m_hi = domain.hi;
out.runs.push_back(std::move(run));
out.count = count;
return true;
}
// The elements of an array bin (bins b[] = {values/ranges}), in order, as runs of
// single-value bins. A range holds the values of the coverpoint type it contains (IEEE
// 1800-2023 19.5.7), while a singleton names a bin even without such a value. Errors on a
// non-constant element. More than --coverage-max-bins bins (e.g. an open '[lo:$]' range over
// a wide coverpoint) are unsupported -- emits COVERIGN, and sets unsupported.
BinRuns arrayBinRuns(AstCoverBin* arrayBinp, AstNodeExpr* exprp) {
BinRuns out;
const int width = runWidth(exprp);
for (AstNode* rangep = arrayBinp->rangesp(); rangep; rangep = rangep->nextp()) {
rangep = V3Const::constifyEdit(rangep);
if (!checkArrayBinElement(arrayBinp, rangep)) return out;
const AstInsideRange* const irp = VN_CAST(rangep, InsideRange);
CrossValueRange range{rangep, resolveWidth(rangep, exprp)};
bool empty = true;
if (!resolveValue(rangep, exprp, true, false, range)) {
rangep->v3warn(E_UNSUPPORTED, "Unsupported: non-integral value in a coverage bin "
"of an integral coverpoint.");
} else {
empty = crossRangeEmpty(range);
}
if (empty && irp) continue; // A range without values contributes no bins
BinRun run{rangep, width, 1};
run.m_empty = empty;
uint64_t count = 1;
if (!empty) {
run.m_lo.opAssign(range.lo);
run.m_hi.opAssign(range.hi);
V3Number span{rangep, width};
span.opSub(run.m_hi, run.m_lo);
// Wider spans exceed any limit
count = span.mostSetBitP1() > 32 ? UINT64_MAX : span.toUQuad() + 1;
}
if (count > v3Global.opt.coverageMaxBins() - out.count) {
arrayBinp->v3warn(COVERIGN, "Unsupported: array 'bins' covering more than "
<< v3Global.opt.coverageMaxBins()
<< " values (e.g. an open '[lo:$]' range over "
"a wide coverpoint); bin "
<< arrayBinp->prettyNameQ() << " ignored\n"
<< arrayBinp->warnMore()
<< "... Suggest a larger --coverage-max-bins");
out.runs.clear();
out.count = 0;
out.unsupported = true;
return out;
}
run.m_count = static_cast<uint32_t>(count);
out.count += run.m_count;
out.runs.push_back(std::move(run));
}
return out;
}
// The bins of a wildcard array (wildcard bins b[] = {...}): one for each coverpoint value
// an element matches (IEEE 1800-2023 19.5.4, 19.5.7), in value order, and named by the value
// (19.5.1), as runs of single-value bins. Errors on a non-constant element. More than
// --coverage-max-bins values are unsupported -- emits COVERIGN, and sets unsupported, and for
// an ignore or illegal array, single. 'report' false omits these diagnostics.
static BinRuns wildcardBinRuns(AstCoverBin* arrayBinp, AstNodeExpr* exprp, bool report) {
BinRuns out;
const int width = runWidth(exprp);
const V3Number one{arrayBinp, width, 1};
// Disjoint runs of the values, in value order, each value once
std::vector<std::pair<V3Number, V3Number>> spans;
uint64_t count = 0; // Values of 'spans'
for (AstNode* rangep = arrayBinp->rangesp(); rangep; rangep = rangep->nextp()) {
rangep = V3Const::constifyEdit(rangep);
if (!checkArrayBinElement(arrayBinp, rangep, report)) {
out.unsupported = true;
return out;
}
CrossValueRange range{rangep, resolveWidth(rangep, exprp)};
if (!resolveValue(rangep, exprp, true, true, range)) {
if (report) {
rangep->v3warn(E_UNSUPPORTED, "Unsupported: non-integral value in a "
"coverage bin of an integral coverpoint.");
}
continue;
}
if (crossRangeEmpty(range)) continue;
std::vector<std::pair<V3Number, V3Number>> found;
crossRangeRuns(range, v3Global.opt.coverageMaxBins(), found);
for (const std::pair<V3Number, V3Number>& run : found) {
// Coverpoint values, sign-extended in both widths
spans.emplace_back(V3Number{rangep, width, run.first},
V3Number{rangep, width, run.second});
}
std::sort(spans.begin(), spans.end(), [](const auto& lhs, const auto& rhs) {
return crossValueLess(lhs.first, rhs.first);
});
std::vector<std::pair<V3Number, V3Number>> merged;
for (const std::pair<V3Number, V3Number>& span : spans) {
// Adjacent or overlapping values join a run; 'first - 1' cannot overflow
V3Number before{rangep, width};
before.opSub(span.first, one);
if (merged.empty() || crossValueLess(merged.back().second, before)) {
merged.push_back(span);
} else if (crossValueLess(merged.back().second, span.second)) {
merged.back().second = span.second;
}
}
count = 0;
for (const std::pair<V3Number, V3Number>& span : merged) {
V3Number size{rangep, width};
size.opSub(span.second, span.first);
// Beyond 2^32 values exceed any limit
count += size.mostSetBitP1() > 32 ? uint64_t{1} << 33 : size.toUQuad() + 1;
if (count > v3Global.opt.coverageMaxBins()) break;
}
spans = std::move(merged);
if (count > v3Global.opt.coverageMaxBins()) {
// An ignore or illegal array still excludes or checks its values, as one bin
out.single = !arrayBinp->binsType().binIsNormal();
if (report) {
arrayBinp->v3warn(
COVERIGN, "Unsupported: wildcard array '"
<< arrayBinp->binsType().verilogKwd()
<< "' of more than --coverage-max-bins of "
<< v3Global.opt.coverageMaxBins() << " values; bin "
<< arrayBinp->prettyNameQ()
<< (out.single ? " treated as one bin" : " ignored") << "\n"
<< arrayBinp->warnMore()
<< "... Suggest a larger --coverage-max-bins");
}
out.unsupported = true;
return out;
}
}
for (const std::pair<V3Number, V3Number>& span : spans) {
out.runs.emplace_back(arrayBinp, width, 0);
BinRun& run = out.runs.back();
run.m_lo = span.first;
run.m_hi = span.second;
// A bin for each value, named by it in the coverpoint's type
V3Number value = span.first;
while (true) {
const V3Number typed{arrayBinp, exprp->width(), value};
out.values.push_back(exprp->isSigned() ? typed.toDecimalS() : typed.toDecimalU());
++run.m_count;
if (value.isCaseEq(span.second)) break;
V3Number next{arrayBinp, width};
value = next.opAdd(value, one);
}
}
out.count = static_cast<uint32_t>(count);
return out;
}
// The runs of an automatic bins declaration, or of an array bin of an integral coverpoint.
// False for other bins, which do not generate as runs, including a wildcard array then one
// bin (see BinRuns::single).
bool binRunsFor(AstCoverBin* binp, AstNodeExpr* exprp, BinRuns& out) {
if (isAutoBins(binp)) {
if (!autoBinRuns(binp, exprp, out)) out.unsupported = true;
return true;
}
if (binp->isArray() && binp->isWildcard()) {
if (!exprp->dtypep()->skipRefp()->isIntegralOrPacked()) {
AstNodeExpr* const falsep = wildcardTypeError(binp, exprp);
VL_DO_DANGLING(pushDeletep(falsep), falsep);
out.unsupported = true;
} else {
out = wildcardBinRuns(binp, exprp, true);
if (out.single) {
binp->isArray(false); // Generates as one bin
return false;
}
}
return true;
}
if (!binp->isArray() || binp->transp() || binp->isWildcard()
|| !exprp->dtypep()->skipRefp()->isIntegralOrPacked()) {
return false;
}
out = arrayBinRuns(binp, exprp);
return true;
}
// Emit a 'this->m_cp->addSingleNamer/addArrayNamer(...)' statement for one bin whose first
// runtime bin index is 'declared'; or with 'valueNames', those naming each bin of an array
AstNodeStmt* makeNamer(AstVar* cpVarp, AstCoverBin* binp, int64_t count, uint32_t declared,
const std::vector<AstNodeExpr*>& values = {},
const std::vector<std::string>& valueNames = {}) {
FileLine* const fl = binp->fileline();
CoverpointBins& bins = m_cpBins.at(cpVarp);
const uint32_t normalCount
= binp->binsType().binIsNormal() ? static_cast<uint32_t>(count < 0 ? 1 : count) : 0;
const BinSpan span{bins.total, normalCount, declared};
if (binp->binsType() == VCoverBinsType::BINS_AUTO_IMPLICIT) {
bins.implicitAuto = span; // Selected by bin, see implicitAutoBinSpan
} else {
bins.spans.emplace(binp->name(), span);
}
bins.total += normalCount;
for (uint32_t i = 0; bins.crossed && i < normalCount; ++i) {
bins.values.push_back({binp, values.empty() ? nullptr : values[i]});
}
// 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 bool single = count < 0;
if (!valueNames.empty()) {
// A bin of a wildcard array is named by its value (IEEE 1800-2023 19.5.1)
AstNodeStmt* stmtsp = nullptr;
for (const std::string& value : valueNames) {
const std::string name
= VIdProtect::protectWordsIf(binp->name(), prot) + "[" + value + "]";
stmtsp = AstNode::addNext(
stmtsp,
itemCall(fl, cpVarp, VCMethod::COVERGROUP_ADD_SINGLE_NAMER,
{ctext(fl, binp->binsType().binSetEnum()), ctext(fl, quoted(name)),
ctext(fl, quoted(VIdProtect::protectIf(fl->filename(), prot))),
cnum(fl, static_cast<uint32_t>(fl->lineno())),
cnum(fl, static_cast<uint32_t>(fl->firstColumn()))})
->makeStmt());
}
return stmtsp;
}
std::vector<AstNodeExpr*> args{ctext(fl, binp->binsType().binSetEnum())};
if (!single) args.push_back(cnum(fl, static_cast<uint32_t>(count))); // value array bin
args.push_back(ctext(fl, quoted(VIdProtect::protectWordsIf(binp->name(), prot))));
args.push_back(ctext(fl, quoted(VIdProtect::protectIf(fl->filename(), prot))));
args.push_back(cnum(fl, static_cast<uint32_t>(fl->lineno())));
args.push_back(cnum(fl, static_cast<uint32_t>(fl->firstColumn())));
return itemCall(fl, cpVarp,
single ? VCMethod::COVERGROUP_ADD_SINGLE_NAMER
: binp->binsType() == VCoverBinsType::BINS_AUTO_IMPLICIT
? VCMethod::COVERGROUP_ADD_NUMBERED_NAMER
: VCMethod::COVERGROUP_ADD_ARRAY_NAMER,
args)
->makeStmt();
}
// 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_<coverpoint> member
uint32_t 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, AstVar* cpVarp, AstNodeExpr* idxp,
bool isNormal) {
return itemCall(fl, cpVarp,
isNormal ? VCMethod::COVERGROUP_INCREMENT_BIN
: VCMethod::COVERGROUP_RECORD_HIT,
{idxp})
->makeStmt();
}
AstNodeStmt* makeRuntimeBinHit(FileLine* fl, const ConvBinTarget& tgt) {
return makeRuntimeBinHit(fl, tgt.cpVarp, cnum(fl, static_cast<uint32_t>(tgt.idx)),
tgt.isNormal);
}
// The condition under which a bin counts a sample: condp, and the bin's iff, and for a
// Normal or default state bin, that the value is not excluded, and the coverpoint's iff
AstNodeExpr* binCondition(AstCoverpoint* coverpointp, AstCoverBin* binp, AstVar* cpVarp,
AstNodeExpr* condp) {
FileLine* const fl = binp->fileline();
if (binp->iffp()) condp = new AstLogAnd{fl, binp->iffp()->cloneTree(false), condp};
const auto excluded = m_excludedVars.find(cpVarp);
if (excluded != m_excludedVars.end() && !binp->transp()
&& (binp->binsType().binIsNormal()
|| binp->binsType() == VCoverBinsType::BINS_DEFAULT)) {
condp = new AstLogAnd{
fl, new AstNot{fl, new AstVarRef{fl, excluded->second, VAccess::READ}}, condp};
}
return applyCoverpointIffCondition(coverpointp, fl, condp);
}
void emitConvHitIf(AstCoverpoint* coverpointp, AstCoverBin* binp, AstVar* cpVarp,
AstNodeExpr* idxp, AstNodeExpr* condp) {
FileLine* const fl = binp->fileline();
AstNode* actionp = makeRuntimeBinHit(fl, cpVarp, idxp, binp->binsType().binIsNormal());
if (binp->binsType() == VCoverBinsType::BINS_ILLEGAL) {
actionp->addNext(makeIllegalBinAction(fl, "Illegal bin " + binp->prettyNameQ()
+ " hit in coverpoint "
+ coverpointp->prettyNameQ()));
}
UASSERT_OBJ(m_sampleFuncp, binp, "sample() CFunc not set for coverpoint");
m_sampleFuncp->addStmtsp(
new AstIf{fl, binCondition(coverpointp, binp, cpVarp, condp), actionp, nullptr});
}
// Emit the sample() code of sized array 'sized': count the value in its bins holding it when
// enabled, as other bins are, and for default bins note in matchedp whether any holds it
void emitSizedSample(AstCoverpoint* coverpointp, AstCoverBin* binp, AstVar* cpVarp,
AstNodeExpr* exprp, uint32_t sized, AstVar* matchedp) {
FileLine* const fl = binp->fileline();
AstNodeExpr* enabledp
= binCondition(coverpointp, binp, cpVarp, new AstConst{fl, AstConst::BitTrue{}});
UASSERT_OBJ(m_sampleFuncp, binp, "sample() CFunc not set for coverpoint");
const bool illegal = binp->binsType() == VCoverBinsType::BINS_ILLEGAL;
if (illegal) {
// The illegal action reads the condition too, which is evaluated once, as the guard
// may have side effects
AstVar* const varp
= new AstVar{fl, VVarType::BLOCKTEMP,
"__VcpEnabled_" + sanitizeGeneratedName(coverpointp->name()) + "_"
+ cvtToStr(sized),
binp->findBitDType()};
varp->funcLocal(true);
m_sampleFuncp->addStmtsp(varp);
m_sampleFuncp->addStmtsp(
new AstAssign{fl, new AstVarRef{fl, varp, VAccess::WRITE}, enabledp});
enabledp = new AstVarRef{fl, varp, VAccess::READ};
}
AstCMethodHard* const callp
= itemCall(fl, cpVarp,
exprp->isWide() ? VCMethod::COVERGROUP_SIZED_SAMPLE_W
: VCMethod::COVERGROUP_SIZED_SAMPLE,
{cnum(fl, sized), exprp->cloneTree(false), enabledp});
callp->dtypeSetBit();
if (illegal) {
m_sampleFuncp->addStmtsp(
new AstIf{fl, new AstLogAnd{fl, callp, enabledp->cloneTree(false)},
makeIllegalBinAction(fl, "Illegal bin " + binp->prettyNameQ()
+ " hit in coverpoint "
+ coverpointp->prettyNameQ())});
} else if (matchedp && binp->binsType().binIsNormal()) {
m_sampleFuncp->addStmtsp(
new AstAssign{fl, new AstVarRef{fl, matchedp, VAccess::WRITE},
new AstOr{fl, new AstVarRef{fl, matchedp, VAccess::READ}, callp}});
} else {
m_sampleFuncp->addStmtsp(callp->makeStmt());
}
}
// A variable local to the constructor
AstVar* constructorTemp(FileLine* fl, const string& name, AstNodeDType* dtypep) {
AstVar* const varp = new AstVar{fl, VVarType::BLOCKTEMP, name, dtypep};
varp->funcLocal(true);
m_constructorp->addStmtsp(varp);
return varp;
}
// Truncate or extend, as its signedness sets, a value to a type
static AstNodeExpr* resizeValue(AstNodeExpr* valuep, AstNodeDType* dtypep) {
FileLine* const fl = valuep->fileline();
if (valuep->width() > dtypep->width()) {
valuep = new AstSel{fl, valuep, 0, dtypep->width()};
} else if (valuep->width() < dtypep->width()) {
valuep = valuep->isSigned()
? static_cast<AstNodeExpr*>(new AstExtendS{fl, valuep, dtypep->width()})
: new AstExtend{fl, valuep, dtypep->width()};
}
valuep->dtypep(dtypep);
return valuep;
}
// The values of a coverpoint, which its name denotes (IEEE 1800-2023 19.5.1.1), as runs in
// value order: an enumerated type's values (6.19), else all values of its type. Bounds
// are at runWidth(), sign-extended like a CrossValueRange's.
static std::vector<std::pair<V3Number, V3Number>> coverpointValues(AstNode* nodep,
AstNodeExpr* exprp) {
const int width = runWidth(exprp);
std::vector<std::pair<V3Number, V3Number>> runs;
const AstEnumDType* const enump = VN_CAST(exprp->dtypep()->skipRefToEnump(), EnumDType);
if (!enump) {
const CrossValueRange domain
= crossValueDomain(nodep, exprp->width(), exprp->isSigned(), width);
runs.emplace_back(domain.lo, domain.hi);
return runs;
}
std::vector<V3Number> values;
for (const AstEnumItem* itemp = enump->itemsp(); itemp;
itemp = VN_AS(itemp->nextp(), EnumItem)) {
const V3Number& num = VN_AS(itemp->valuep(), Const)->num();
if (num.isFourState()) continue; // Not a coverpoint value (19.5.7)
values.emplace_back(nodep, width);
if (exprp->isSigned()) {
values.back().opExtendS(num, num.width());
} else {
values.back().opAssign(num);
}
}
std::sort(values.begin(), values.end(), crossValueLess);
const V3Number one{nodep, width, 1};
for (const V3Number& value : values) {
V3Number next{nodep, width};
if (!runs.empty() && next.opAdd(runs.back().second, one).isCaseEq(value)) {
runs.back().second = value;
} else {
runs.emplace_back(value, value);
}
}
return runs;
}
// Emit the constructor code building the bins 'binp' whose values it computes: the values of
// each element that are coverpoint values (IEEE 1800-2023 19.5.7), those a 'with' filter
// keeps (19.5.1.1), then its bins
void generateConstructedBins(AstCoverpoint* coverpointp, AstCoverBin* binp, AstVar* cpVarp,
AstNodeExpr* exprp) {
FileLine* const fl = binp->fileline();
const string prefix
= m_sizedNames.get(sanitizeGeneratedName(coverpointp->name() + "__" + binp->name()));
AstVar* countp = nullptr;
if (AstNodeExpr* const sizep = binp->arraySizep()) {
countp = constructorTemp(fl, prefix + "_count", sizep->dtypep());
m_constructorp->addStmtsp(new AstAssign{fl, new AstVarRef{fl, countp, VAccess::WRITE},
sizep->cloneTree(false)});
}
AstCoverWith* const withp = binWith(binp);
if (withp && !binRangesp(binp)) { // The coverpoint's name: all of its values
for (const std::pair<V3Number, V3Number>& run : coverpointValues(binp, exprp)) {
m_constructorp->addStmtsp(itemCall(fl, cpVarp,
exprp->isWide()
? VCMethod::COVERGROUP_SIZED_RANGE_W
: VCMethod::COVERGROUP_SIZED_RANGE,
{newValueConst(fl, run.first, exprp),
newValueConst(fl, run.second, exprp)})
->makeStmt());
}
}
uint32_t element = 0;
for (AstNode* rangep = binRangesp(binp); rangep; rangep = rangep->nextp()) {
if (VN_IS(rangep, Unbounded)) { // A parameter of '$'
binp->v3error("Bins value may not be '$', which may only bound a range "
"(IEEE 1800-2023 6.20.7)");
continue;
}
generateSizedElement(cpVarp, binp, rangep, exprp, prefix + "_" + cvtToStr(element++));
}
if (withp) generateWithFilter(binp, withp, cpVarp, exprp, prefix);
AstNodeExpr* countValuep;
AstNodeExpr* positivep;
if (countp) {
const auto countRef = [&]() { return new AstVarRef{fl, countp, VAccess::READ}; };
AstConst* const zerop = new AstConst{fl, AstConst::DTyped{}, countp->dtypep()};
positivep = countp->isSigned()
? static_cast<AstNodeExpr*>(new AstGtS{fl, countRef(), zerop})
: new AstNeq{fl, countRef(), zerop};
// Saturate a count wider than 64 bits: min(N, T) is unchanged, or over any limit
countValuep = resizeValue(countRef(), countp->findUInt64DType());
if (countp->width() > VL_QUADSIZE) {
countValuep = new AstCond{
fl,
new AstRedOr{fl, new AstSel{fl, countRef(), VL_QUADSIZE,
countp->width() - VL_QUADSIZE}},
new AstConst{fl, AstConst::Unsized64{}, std::numeric_limits<uint64_t>::max()},
countValuep};
countValuep->dtypeSetUInt64();
}
} else { // Of a filter's scalar bin, or bin per value
countValuep = new AstConst{fl, AstConst::Unsized64{}, 1};
positivep = new AstConst{fl, AstConst::BitTrue{}};
}
std::vector<AstNodeExpr*> args{ctext(fl, binp->binsType().binSetEnum()), countValuep,
positivep};
// A filter's bins have the limit it began with
if (!withp) args.push_back(cnum(fl, v3Global.opt.coverageMaxBins()));
const bool prot = v3Global.opt.protectIds();
args.push_back(ctext(fl, quoted(VIdProtect::protectWordsIf(binp->name(), prot))));
args.push_back(ctext(fl, quoted(VIdProtect::protectIf(fl->filename(), prot))));
args.push_back(cnum(fl, static_cast<uint32_t>(fl->lineno())));
args.push_back(cnum(fl, static_cast<uint32_t>(fl->firstColumn())));
m_constructorp->addStmtsp(
itemCall(fl, cpVarp,
withp ? VCMethod::COVERGROUP_WITH_FINISH : VCMethod::COVERGROUP_SIZED_FINISH,
args)
->makeStmt());
}
// Emit the constructor code evaluating the 'with' filter of 'binp' for each of its candidate
// values, which sizedRange() added, passing the runs of values it keeps (IEEE 1800-2023
// 19.5.1.1). The filter is evaluated in a loop, whose code does not grow with the elements:
// withBegin(grouping, limit);
// more = 1;
// while (withNext()) {
// value = withLo(); last = withHi(); run = 0;
// while (true) {
// item = value;
// if (filter) { if (!run) { first = value; run = 1; } }
// else if (run) { more = withRun(first, value - 1); run = 0; }
// if (!more || value == last) break;
// ++value;
// }
// if (run) more = withRun(first, last);
// }
void generateWithFilter(AstCoverBin* binp, AstCoverWith* withp, AstVar* cpVarp,
AstNodeExpr* exprp, const string& prefix) {
FileLine* const fl = withp->fileline();
const bool wide = exprp->isWide();
const string grouping = !binp->isArray() ? "Single"
: binp->arraySizep() ? "Fixed"
: "Values";
m_constructorp->addStmtsp(itemCall(fl, cpVarp, VCMethod::COVERGROUP_WITH_BEGIN,
{ctext(fl, "VlCovBinGrouping::" + grouping),
cnum(fl, v3Global.opt.coverageMaxBins())})
->makeStmt());
// The candidates count in the coverpoint's width, and the filter reads each as 'item',
// of the coverpoint's type, so that a filter changing 'item' cannot change the loop
AstNodeDType* const valueDTypep
= exprp->findLogicDType(exprp->width(), exprp->width(),
exprp->isSigned() ? VSigning::SIGNED : VSigning::UNSIGNED);
AstVar* const valuep = constructorTemp(fl, prefix + "_value", valueDTypep);
AstVar* const lastp = constructorTemp(fl, prefix + "_last", valueDTypep);
AstVar* const firstp = constructorTemp(fl, prefix + "_first", valueDTypep);
AstVar* const runp = constructorTemp(fl, prefix + "_run", binp->findBitDType());
AstVar* const morep = constructorTemp(fl, prefix + "_more", binp->findBitDType());
AstVar* const itemp = withp->itemp()->unlinkFrBack();
itemp->name(prefix + "_item");
m_constructorp->addStmtsp(itemp);
const auto ref = [&](AstVar* varp) { return new AstVarRef{fl, varp, VAccess::READ}; };
const auto assign = [&](AstVar* varp, AstNodeExpr* rhsp) -> AstNode* {
return new AstAssign{fl, new AstVarRef{fl, varp, VAccess::WRITE}, rhsp};
};
const auto flag = [&](AstVar* varp, bool value) {
return assign(varp, value ? new AstConst{fl, AstConst::BitTrue{}}
: new AstConst{fl, AstConst::BitFalse{}});
};
const auto bound = [&](VCMethod narrow, VCMethod wideMethod, AstVar* varp) -> AstNode* {
if (wide) {
return itemCall(fl, cpVarp, wideMethod, {new AstVarRef{fl, varp, VAccess::WRITE}})
->makeStmt();
}
AstCMethodHard* const callp = itemCall(fl, cpVarp, narrow);
callp->dtypeSetUInt64();
return assign(varp, resizeValue(callp, valueDTypep));
};
const auto keep = [&](AstNodeExpr* lop, AstNodeExpr* hip) {
AstCMethodHard* const callp = itemCall(
fl, cpVarp, wide ? VCMethod::COVERGROUP_WITH_RUN_W : VCMethod::COVERGROUP_WITH_RUN,
{lop, hip});
callp->dtypeSetBit();
return assign(morep, callp);
};
const auto step = [&](bool up) {
AstConst* const onep = new AstConst{fl, AstConst::WidthedValue{}, exprp->width(), 1};
AstNodeExpr* const stepp
= up ? static_cast<AstNodeExpr*>(new AstAdd{fl, ref(valuep), onep})
: new AstSub{fl, ref(valuep), onep};
stepp->dtypep(valueDTypep);
return stepp;
};
AstLoop* const innerp = new AstLoop{fl};
innerp->addStmtsp(assign(itemp, ref(valuep)));
innerp->addStmtsp(new AstIf{
fl, withp->filterp()->unlinkFrBack(),
new AstIf{fl, new AstNot{fl, ref(runp)},
assign(firstp, ref(valuep))->addNext(flag(runp, true))},
new AstIf{fl, ref(runp), keep(ref(firstp), step(false))->addNext(flag(runp, false))}});
innerp->addStmtsp(new AstLoopTest{
fl, innerp, new AstLogAnd{fl, ref(morep), new AstNeq{fl, ref(valuep), ref(lastp)}}});
innerp->addStmtsp(assign(valuep, step(true)));
AstLoop* const outerp = new AstLoop{fl};
AstCMethodHard* const nextp = itemCall(fl, cpVarp, VCMethod::COVERGROUP_WITH_NEXT);
nextp->dtypeSetBit();
outerp->addStmtsp(new AstLoopTest{fl, outerp, nextp});
outerp->addStmtsp(
bound(VCMethod::COVERGROUP_WITH_LO, VCMethod::COVERGROUP_WITH_LO_W, valuep));
outerp->addStmtsp(
bound(VCMethod::COVERGROUP_WITH_HI, VCMethod::COVERGROUP_WITH_HI_W, lastp));
outerp->addStmtsp(flag(runp, false));
outerp->addStmtsp(innerp);
outerp->addStmtsp(new AstIf{fl, ref(runp), keep(ref(firstp), ref(lastp))});
m_constructorp->addStmtsp(flag(morep, true));
m_constructorp->addStmtsp(outerp);
}
// Emit 'sizedRange(lo, hi)' for the coverpoint values of an element of bins 'binp', whose
// values the constructor computes: resolved now if constant, else when constructed by
// clipping to the coverpoint's values. A wildcard pattern's values give a range for each
// run of them. 'prefix' names its temporaries.
void generateSizedElement(AstVar* cpVarp, const AstCoverBin* binp, AstNode* rangep,
AstNodeExpr* exprp, const string& prefix) {
FileLine* const fl = rangep->fileline();
const VCMethod method = exprp->isWide() ? VCMethod::COVERGROUP_SIZED_RANGE_W
: VCMethod::COVERGROUP_SIZED_RANGE;
const AstInsideRange* const irp = VN_CAST(rangep, InsideRange);
AstNodeExpr* const lowp = irp ? irp->lhsp() : VN_AS(rangep, NodeExpr);
AstNodeExpr* const highp = irp ? irp->rhsp() : nullptr;
const auto unbounded
= [](const AstNodeExpr* boundp) { return !boundp || VN_IS(boundp, Unbounded); };
const auto constant
= [&](const AstNodeExpr* boundp) { return unbounded(boundp) || VN_IS(boundp, Const); };
const auto integral = [&](const AstNodeExpr* boundp) {
return unbounded(boundp) || boundp->dtypep()->skipRefp()->isIntegralOrPacked();
};
if (constant(lowp) && constant(highp)) {
const auto fourState = [](const AstNodeExpr* boundp) {
const AstConst* const constp = VN_CAST(boundp, Const);
return constp && constp->num().isFourState();
};
if (irp && (fourState(lowp) || fourState(highp))) {
rangep->v3error("Four-state (x/z) value in "
<< (binp->isArray() ? "array bins" : "bin")
<< " range bound; range bounds must be two-state constants");
return;
}
CrossValueRange range{rangep, resolveWidth(rangep, exprp)};
if (!resolveValue(rangep, exprp, true, binp->isWildcard(), range)) {
rangep->v3warn(E_UNSUPPORTED, "Unsupported: non-integral value in a coverage bin "
"of an integral coverpoint.");
} else if (!crossRangeEmpty(range)) {
std::vector<std::pair<V3Number, V3Number>> runs{{range.lo, range.hi}};
if (range.wildcard) { // checkConstructedBins bounded the runs
runs.clear();
crossRangeRuns(range, v3Global.opt.coverageMaxBins(), runs);
}
for (const std::pair<V3Number, V3Number>& run : runs) {
m_constructorp->addStmtsp(itemCall(fl, cpVarp, method,
{newValueConst(fl, run.first, exprp),
newValueConst(fl, run.second, exprp)})
->makeStmt());
}
}
return;
}
if (!integral(lowp) || !integral(highp)) {
rangep->v3warn(E_UNSUPPORTED, "Unsupported: non-integral value in a coverage bin "
"of an integral coverpoint.");
return;
}
// Compare values and the coverpoint's domain signed, in a width holding all of them
const auto boundWidth
= [&](const AstNodeExpr* boundp) { return unbounded(boundp) ? 0 : boundp->width(); };
const int width = std::max({exprp->width(), boundWidth(lowp), boundWidth(highp)}) + 1;
const CrossValueRange domain
= crossValueDomain(rangep, exprp->width(), exprp->isSigned(), width);
AstVar* const lop = constructorTemp(fl, prefix + "_lo", exprp->dtypep());
lop->dtypeSetLogicSized(width, VSigning::SIGNED);
AstVar* const hip = constructorTemp(fl, prefix + "_hi", lop->dtypep());
const auto bound = [&](AstNodeExpr* boundp, const V3Number& limit) -> AstNodeExpr* {
if (unbounded(boundp)) {
AstConst* const limitp = new AstConst{fl, limit};
limitp->dtypeFrom(lop);
return limitp;
}
AstNodeExpr* valuep = boundp->cloneTree(false);
// A value no wider than a signed coverpoint has its type (see crossRangeBound)
if (exprp->isSigned() && boundp->width() <= exprp->width()) {
valuep = resizeValue(valuep, exprp->dtypep());
}
return resizeValue(valuep, lop->dtypep());
};
const auto ref = [&](AstVar* varp, VAccess access = VAccess::READ) {
return new AstVarRef{fl, varp, access};
};
m_constructorp->addStmtsp(
new AstAssign{fl, ref(lop, VAccess::WRITE), bound(lowp, domain.lo)});
m_constructorp->addStmtsp(
new AstAssign{fl, ref(hip, VAccess::WRITE), irp ? bound(highp, domain.hi) : ref(lop)});
AstConst* const minp = new AstConst{fl, domain.lo};
AstConst* const maxp = new AstConst{fl, domain.hi};
minp->dtypeFrom(lop);
maxp->dtypeFrom(lop);
AstCond* const lowerp
= new AstCond{fl, new AstLtS{fl, ref(lop), minp}, minp->cloneTree(false), ref(lop)};
AstCond* const upperp
= new AstCond{fl, new AstGtS{fl, ref(hip), maxp}, maxp->cloneTree(false), ref(hip)};
lowerp->dtypeFrom(lop);
upperp->dtypeFrom(lop);
m_constructorp->addStmtsp(new AstAssign{fl, ref(lop, VAccess::WRITE), lowerp});
m_constructorp->addStmtsp(new AstAssign{fl, ref(hip, VAccess::WRITE), upperp});
m_constructorp->addStmtsp(new AstIf{fl, new AstLteS{fl, ref(lop), ref(hip)},
itemCall(fl, cpVarp, method,
{resizeValue(ref(lop), exprp->dtypep()),
resizeValue(ref(hip), exprp->dtypep())})
->makeStmt()});
}
// The runtime index of the bin of a run holding the coverpoint value, which is in the run:
// declared + (value - lo) / stride, capped at the last bin, which holds any remainder.
static AstNodeExpr* runBinIndex(FileLine* fl, AstNodeExpr* exprp, const BinRun& run) {
if (run.m_count == 1) return cnum(fl, run.m_declared);
const int width = exprp->width();
// A run spans at most 2^width values, so offsets in it are unsigned width-bit numbers
AstNodeExpr* indexp
= new AstSub{fl, exprp->cloneTree(false), newValueConst(fl, run.m_lo, exprp)};
indexp->dtypeSetLogicSized(width, VSigning::UNSIGNED);
V3Number stride{fl, width, 0};
stride.opAssign(run.m_stride);
if (stride.countOnes() != 1) {
indexp = new AstDiv{fl, indexp, new AstConst{fl, stride}};
} else if (!stride.isEqOne()) {
indexp = new AstShiftR{fl, indexp, new AstConst{fl, stride.mostSetBitP1() - 1}};
}
// Compare the run's values with those of count bins of stride values, without overflow
const int extWidth = run.m_lo.width() + 1;
V3Number lo{fl, extWidth, 0};
lo.opExtendS(run.m_lo, run.m_lo.width());
V3Number span{fl, extWidth, 0};
span.opExtendS(run.m_hi, run.m_hi.width());
span.opSub(V3Number{span}, lo);
V3Number covered{fl, extWidth, 0};
covered.opAssign(run.m_stride);
covered.opMul(V3Number{covered}, V3Number{fl, extWidth, run.m_count});
V3Number hasRemainder{fl, 1, 0};
if (!hasRemainder.opGte(span, covered).isEqZero()) {
AstConst* const lastp = new AstConst{fl, V3Number{fl, width, run.m_count - 1}};
indexp = new AstCond{fl, new AstGt{fl, indexp->cloneTree(false), lastp},
lastp->cloneTree(false), indexp};
}
if (width < VL_IDATASIZE) {
indexp = new AstExtend{fl, indexp, VL_IDATASIZE};
} else if (width > VL_IDATASIZE) {
indexp = new AstSel{fl, indexp, 0, VL_IDATASIZE};
}
return new AstAdd{fl, cnum(fl, run.m_declared), indexp};
}
// Emit the sample() hit of a run of bins, whose code does not grow with its number of bins:
// if (iff && lo <= value && value <= hi) m_cp.incrementBin(<runBinIndex>);
void emitRunHit(AstCoverpoint* coverpointp, AstCoverBin* binp, AstVar* cpVarp,
AstNodeExpr* exprp, const BinRun& run) {
FileLine* const fl = binp->fileline();
AstConst* const lop = newValueConst(fl, run.m_lo, exprp);
AstNodeExpr* condp = nullptr;
if (run.m_lo.isCaseEq(run.m_hi)) {
condp = new AstEq{fl, exprp->cloneTree(false), lop};
} else {
AstConst* const hip = newValueConst(fl, run.m_hi, exprp);
condp = makeRangeCondition(fl, exprp, lop, hip);
VL_DO_DANGLING(pushDeletep(lop), lop);
VL_DO_DANGLING(pushDeletep(hip), hip);
}
emitConvHitIf(coverpointp, binp, cpVarp, runBinIndex(fl, exprp, run), condp);
}
// 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();
const bool dynamic = m_runtimePoints.count(coverpointp);
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(),
coverpointDType(fl, static_cast<uint32_t>(hitBound))};
m_covergroupp->addMembersp(cpVarp);
m_cpVarMap[coverpointp->name()] = cpVarp;
m_cpBins.emplace(cpVarp, CoverpointBins{});
m_cpBins.at(cpVarp).exprp = exprp;
m_cpBins.at(cpVarp).crossed = crossFed;
// Create the runtime in the instance node first; everything below configures it.
m_constructorp->addStmtsp(makeItemCreate(fl, cpVarp, VCMethod::COVERGROUP_ADD_COVERPOINT));
generateItemWeight(fl, cpVarp, coverpointp->optionsp());
// 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) {
UASSERT_OBJ(m_sampleFuncp, coverpointp, "sample() CFunc not set for clearHitList");
m_sampleFuncp->addStmtsp(
itemCall(fl, cpVarp, VCMethod::COVERGROUP_CLEAR_HIT_LIST)->makeStmt());
}
if (dynamic && coverpointHasStateExclusions(coverpointp)) {
AstVar* const excludedp
= new AstVar{fl, VVarType::BLOCKTEMP,
"__VcpExcluded_" + sanitizeGeneratedName(coverpointp->name()),
coverpointp->findBitDType()};
excludedp->funcLocal(true);
m_sampleFuncp->addStmtsp(excludedp);
AstCMethodHard* const callp
= itemCall(fl, cpVarp,
exprp->isWide() ? VCMethod::COVERGROUP_VALUE_EXCLUDED_W
: VCMethod::COVERGROUP_VALUE_EXCLUDED,
{exprp->cloneTree(false)});
callp->dtypeSetBit();
m_sampleFuncp->addStmtsp(
new AstAssign{fl, new AstVarRef{fl, excludedp, VAccess::WRITE}, callp});
m_excludedVars.emplace(cpVarp, excludedp);
}
// Walk bins (non-default, then default), assigning sequential indices that match the
// namer append order; emit sample increments and collect namer statements. Constructed
// bins follow them all, placed when the coverpoint is constructed.
std::vector<AstNodeStmt*> namerStmts;
std::vector<AstCoverBin*> defaultBins;
std::vector<AstCoverBin*> sizedBins;
std::vector<std::tuple<AstCoverBin*, uint32_t, AstNodeExpr*>> metadata;
std::vector<const BinRun*> runMetadata;
uint64_t idx = 0; // Runtime index of the next bin; 32-bit once checked below
for (AstNode* binp = coverpointp->binsp(); binp; binp = binp->nextp()) {
AstCoverBin* const cbinp = VN_AS(binp, CoverBin);
const int errorsBefore = dynamic ? V3Error::errorCount() : 0;
if (cbinp->binsType() == VCoverBinsType::BINS_DEFAULT) {
defaultBins.push_back(cbinp);
continue;
}
if (isConstructedBins(cbinp)) {
UASSERT_OBJ(dynamic, cbinp, "Constructed bins without value metadata");
BinSpan span;
span.sized = static_cast<int32_t>(sizedBins.size());
m_cpBins.at(cpVarp).spans.emplace(cbinp->name(), span);
sizedBins.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, static_cast<uint32_t>(idx)));
const ConvBinTarget tgt{cpVarp, static_cast<uint32_t>(idx),
cbinp->binsType().binIsNormal()};
for (AstNode* sp = cbinp->transp(); sp; sp = sp->nextp())
generateSingleTransitionCode(coverpointp, cbinp, exprp, tgt,
VN_AS(sp, CoverTransSet));
if (dynamic && V3Error::errorCount() == errorsBefore) {
metadata.emplace_back(cbinp, static_cast<uint32_t>(idx), nullptr);
}
++idx;
continue;
}
BinRuns plan;
if (binRunsFor(cbinp, exprp, plan)) {
// Array elements and automatic bins generate as runs, so neither sample() nor
// the constructor grows with their number of bins.
if (plan.unsupported) { // bin ignored or invalid; reserve no slot
m_droppedBins[coverpointp].push_back(cbinp->name());
continue;
}
CoverpointBins& bins = m_cpBins.at(cpVarp);
const uint32_t firstValue = bins.total;
const uint32_t firstDeclared = static_cast<uint32_t>(idx);
namerStmts.push_back(
makeNamer(cpVarp, cbinp, plan.count, firstDeclared, {}, plan.values));
for (BinRun& run : plan.runs) {
run.m_declared = static_cast<uint32_t>(idx);
bins.runs.push_back(std::move(run));
const BinRun& stored = bins.runs.back();
if (bins.crossed && cbinp->binsType().binIsNormal()) {
const uint32_t first = firstValue + stored.m_declared - firstDeclared;
for (uint32_t element = 0; element < stored.m_count; ++element) {
bins.values[first + element].runp = &stored;
bins.values[first + element].element = element;
}
}
if (!stored.m_empty) {
emitRunHit(coverpointp, cbinp, cpVarp, exprp, stored);
if (dynamic && V3Error::errorCount() == errorsBefore) {
runMetadata.push_back(&stored);
}
}
idx += stored.m_count;
}
continue;
}
if (cbinp->isArray()) { // value array of a real coverpoint: b[0]..b[N-1]
// Only integral coverpoints have runtime value metadata (m_runtimePoints)
UASSERT_OBJ(!dynamic, cbinp, "Runtime value metadata for a real coverpoint");
bool unsupported = false;
std::vector<AstNodeExpr*> values = extractArrayValues(cbinp, exprp, unsupported);
if (unsupported) { // bin ignored (COVERIGN emitted); reserve no slot
m_droppedBins[coverpointp].push_back(cbinp->name());
continue;
}
namerStmts.push_back(makeNamer(cpVarp, cbinp, static_cast<int64_t>(values.size()),
static_cast<uint32_t>(idx), values));
for (AstNodeExpr* valuep : values) {
// The cross selections of this covergroup still read the value.
m_detachedValues.push_back(valuep);
emitConvHitIf(coverpointp, cbinp, cpVarp,
cnum(cbinp->fileline(), static_cast<uint32_t>(idx)),
buildValueCondition(cbinp, exprp, valuep));
++idx;
}
} else {
namerStmts.push_back(makeNamer(cpVarp, cbinp, -1, static_cast<uint32_t>(idx)));
// 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,
cnum(cbinp->fileline(), static_cast<uint32_t>(idx)), condp);
if (dynamic && V3Error::errorCount() == errorsBefore) {
metadata.emplace_back(cbinp, static_cast<uint32_t>(idx), nullptr);
}
++idx;
}
}
// A cross selecting an ignored bins declaration selects no bins
for (const std::string& name : m_droppedBins[coverpointp]) {
m_cpBins.at(cpVarp).spans.emplace(name, BinSpan{});
}
// A value a constructed bin holds is no default bin's; only sampling tells which do
AstVar* sizedMatchedp = nullptr;
if (!defaultBins.empty()
&& std::any_of(sizedBins.begin(), sizedBins.end(), [](const AstCoverBin* binp) {
return binp->binsType().binIsNormal();
})) {
sizedMatchedp = new AstVar{fl, VVarType::BLOCKTEMP,
"__VcpSized_" + sanitizeGeneratedName(coverpointp->name()),
coverpointp->findBitDType()};
sizedMatchedp->funcLocal(true);
m_sampleFuncp->addStmtsp(sizedMatchedp);
m_sampleFuncp->addStmtsp(
new AstAssign{fl, new AstVarRef{fl, sizedMatchedp, VAccess::WRITE},
new AstConst{fl, AstConst::BitFalse{}}});
}
for (uint32_t sized = 0; sized < sizedBins.size(); ++sized) {
emitSizedSample(coverpointp, sizedBins[sized], cpVarp, exprp, sized, sizedMatchedp);
}
for (AstCoverBin* const defBinp : defaultBins) {
FileLine* const dfl = defBinp->fileline();
namerStmts.push_back(makeNamer(cpVarp, defBinp, -1, static_cast<uint32_t>(idx)));
AstNodeExpr* condp = buildDefaultCondition(coverpointp, exprp, dfl);
if (sizedMatchedp) {
condp = new AstLogAnd{
dfl, new AstNot{dfl, new AstVarRef{dfl, sizedMatchedp, VAccess::READ}}, condp};
}
emitConvHitIf(coverpointp, defBinp, cpVarp, cnum(dfl, static_cast<uint32_t>(idx)),
condp);
++idx;
}
if (idx > std::numeric_limits<uint32_t>::max()) {
// The runtime indexes bins with 32 bits; stop before generating a model
coverpointp->v3warn(E_UNSUPPORTED, "Unsupported: coverpoint with more than "
<< std::numeric_limits<uint32_t>::max()
<< " bins");
}
// 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);
m_constructorp->addStmtsp(
itemCall(fl, cpVarp, VCMethod::COVERGROUP_INIT,
{ctext(fl, quoted(hier)), cnum(fl, static_cast<uint32_t>(atLeastValue)),
cnum(fl, static_cast<uint32_t>(idx))})
->makeStmt());
for (AstNodeStmt* const ns : namerStmts) m_constructorp->addStmtsp(ns);
if (dynamic) {
m_constructorp->addStmtsp(
itemCall(fl, cpVarp, VCMethod::COVERGROUP_VALUE_TYPE,
{cnum(fl, exprp->width()), cnum(fl, exprp->isSigned())})
->makeStmt());
ValueLists lists;
for (const auto& entry : metadata) {
collectValueMetadata(lists, exprp, std::get<0>(entry), std::get<1>(entry),
std::get<2>(entry));
}
for (const BinRun* const runp : runMetadata) collectRunMetadata(lists, exprp, *runp);
emitValueList(fl, cpVarp, VCMethod::COVERGROUP_VALUE_RANGES, lists.m_ranges);
emitValueList(fl, cpVarp, VCMethod::COVERGROUP_VALUE_RUNS, lists.m_runs);
emitValueList(fl, cpVarp, VCMethod::COVERGROUP_VALUE_PATTERNS, lists.m_patterns);
emitValueList(fl, cpVarp, VCMethod::COVERGROUP_VALUE_TRANSITIONS, lists.m_transitions);
for (AstCoverBin* const binp : sizedBins) {
generateConstructedBins(coverpointp, binp, cpVarp, exprp);
}
m_constructorp->addStmtsp(
itemCall(fl, cpVarp, VCMethod::COVERGROUP_VALUE_FINALIZE)->makeStmt());
}
if (v3Global.opt.coverage()) {
const std::string page
= VIdProtect::protectIf("v_covergroup/" + m_covergroupp->name(), prot);
m_constructorp->addStmtsp(
itemCall(fl, cpVarp, VCMethod::COVERGROUP_REGISTER_BINS,
{ctext(fl, "vlSymsp->_vm_contextp__->coveragep()"),
ctext(fl, quoted(page)),
cnum(fl, itemDatabaseWeight(coverpointp->optionsp())),
cnum(fl, m_cgTypeWeight)})
->makeStmt());
}
}
// 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<AstCoverTransItem*>& 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<AstCoverTransItem*>& 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<uint32_t>(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<uint32_t>(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<AstNode*>(errorp)->addNext(new AstStop{fl, true});
return errorp;
}
// Preserve the coverpoint's width and signedness after V3Width.
static AstConst* newValueConst(FileLine* fl, const V3Number& value, const AstNodeExpr* exprp) {
V3Number narrowed{fl, exprp->width(), 0};
narrowed.opAssign(value);
AstConst* const constp = new AstConst{fl, narrowed};
constp->dtypeFrom(exprp);
return constp;
}
// A real copy of a real or integral range bound, for comparing with a real coverpoint.
static AstConst* newRealConst(AstConst* constp) {
if (constp->num().isDouble()) return constp->cloneTree(false);
V3Number real{&constp->num(), 64};
real.opIToRD(constp->num(), constp->isSigned());
return new AstConst{constp->fileline(), real};
}
// 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 domain bounds.
// 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);
if (exprp->isDouble()) {
// A real coverpoint has no finite domain bounds to omit.
return new AstAnd{fl,
new AstGteD{fl, exprp->cloneTree(false), newRealConst(minConstp)},
new AstLteD{fl, exprp->cloneTree(false), newRealConst(maxConstp)}};
}
// 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);
V3Number minimum{fl, exprWidth, 0};
V3Number maximum{fl, exprWidth, 0};
maximum.setAllBits1();
if (exprp->isSigned()) {
minimum.setBit(exprWidth - 1, 1);
maximum.setBit(exprWidth - 1, 0);
}
AstNodeExpr* lowerp = nullptr;
AstNodeExpr* upperp = nullptr;
if (minWidep->num().isCaseEq(minimum)) {
VL_DO_DANGLING(pushDeletep(minWidep), minWidep);
} else {
lowerp = exprp->isSigned() ? static_cast<AstNodeExpr*>(
new AstGteS{fl, exprp->cloneTree(false), minWidep})
: static_cast<AstNodeExpr*>(
new AstGte{fl, exprp->cloneTree(false), minWidep});
}
if (maxWidep->num().isCaseEq(maximum)) {
VL_DO_DANGLING(pushDeletep(maxWidep), maxWidep);
} else {
upperp = exprp->isSigned() ? static_cast<AstNodeExpr*>(
new AstLteS{fl, exprp->cloneTree(false), maxWidep})
: static_cast<AstNodeExpr*>(
new AstLte{fl, exprp->cloneTree(false), maxWidep});
}
if (lowerp && upperp) return new AstAnd{fl, lowerp, upperp};
if (lowerp) return lowerp;
if (upperp) return upperp;
return new AstConst{fl, AstConst::BitTrue{}};
}
// 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;
valp = V3Const::constifyEdit(valp);
AstConst* const constp = VN_CAST(valp, Const);
if (!constp) {
valp->v3error("Non-constant expression in transition bin; "
"values must be constants (IEEE 1800-2023 19.5)");
return new AstConst{valp->fileline(), AstConst::BitFalseErroring{}};
}
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<AstCoverTransItem*> 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, ...}; <call> }" statement. The brace
// and the temporary array stay literal text -- a CMethodHard is one call, not a block --
// but callp itself carries the member, method and '->'. Construction only: init() copies
// the array into the cross, so sample() reads it from there and needs no array at all.
AstCStmt* makeCrossCpsCall(FileLine* fl, const std::vector<AstVar*>& cpVars,
AstCMethodHard* callp) {
AstCStmt* const cs = new AstCStmt{fl};
cs->add("{ VlCoverpoint* __Vcx_cps[] = {");
for (size_t d = 0; d < cpVars.size(); ++d) {
if (d != 0) cs->add(", ");
cs->add(memberRef(fl, cpVars[d]));
}
cs->add("}; ");
cs->add(callp);
cs->add("; }");
return cs;
}
// Assign the per-bin flags individually: one-bit SV results have integer C++ storage types,
// which may narrow in a bool initializer list but convert implicitly in assignments.
AstCStmt* makeCrossIffsCall(FileLine* fl, const std::vector<AstCoverCrossBin*>& bins,
AstCMethodHard* callp) {
AstCStmt* const cs = new AstCStmt{fl};
cs->add("{ bool __Vcx_iffs[" + cvtToStr(bins.size()) + "]; ");
for (size_t i = 0; i < bins.size(); ++i) {
const AstCoverCrossBin* const binp = bins[i];
cs->add("__Vcx_iffs[" + cvtToStr(i) + "] = ");
cs->add(binp->iffp() ? binp->iffp()->cloneTree(false)
: new AstConst{fl, AstConst::BitTrue{}});
cs->add("; ");
}
cs->add(callp);
cs->add("; }");
return cs;
}
using CrossSelection = std::vector<uint64_t>;
struct ResolvedCrossBin final {
AstCoverCrossBin* binp;
CrossSelection selection;
};
struct CrossLayout final {
uint32_t tuples = 0;
uint32_t autoBins = 0;
uint64_t binWords = 0;
bool valid = true;
std::vector<ResolvedCrossBin> bins;
};
struct CrossSelectionContext final {
AstCoverCross* crossp; // Cross whose tuple space is being selected
const std::vector<AstVar*>& cpVars; // Feeding coverpoints in dimension order
const std::map<std::string, uint32_t>& dimensions; // Coverpoint name -> dimension
uint32_t tuples; // Size of the Cartesian product
std::vector<uint32_t> strides; // Flat-index stride per dimension
bool valid = true; // False if this explicit bin cannot be implemented
};
struct CrossBinsofTarget final {
const CoverpointBins* m_binsp = nullptr; // Declared bins; null after a reported error
uint32_t m_dimension = 0; // Coverpoint index within the cross
uint32_t m_first = 0; // First declared normal bin in the selected span
uint32_t m_count = 0; // Number of declared normal bins in the selected span
uint32_t m_declaredFirst = 0; // First runtime bin index of the selected span
uint32_t m_declaredEnd = UINT32_MAX; // One past the last runtime bin index
int32_t m_sized = -1; // Index of the sized array holding the selected bins; or -1
};
struct CrossValueRange final {
V3Number lo; // Inclusive lower bound, sign-extended to the comparison width
V3Number hi; // Inclusive upper bound
V3Number pattern; // Allowed bit values; all X for an ordinary interval
bool wildcard = false; // A wildcard singleton rather than an exact four-state value
CrossValueRange(AstNode* nodep, int width)
: lo{nodep, width}
, hi{nodep, width}
, pattern{nodep, width} {
pattern.setAllBitsX();
}
};
static std::vector<AstNode*> crossBinValues(const CrossBinValues& bin) {
if (bin.valuep) return {bin.valuep};
std::vector<AstNode*> values;
if (bin.binp->transp()) {
// IEEE 1800-2023 19.6.1: binsof uses the last value of each transition.
for (AstNode* setp = bin.binp->transp(); setp; setp = setp->nextp()) {
AstCoverTransItem* lastp = VN_AS(setp, CoverTransSet)->itemsp();
while (lastp->nextp()) lastp = VN_AS(lastp->nextp(), CoverTransItem);
for (AstNode* valuep = lastp->valuesp(); valuep; valuep = valuep->nextp()) {
values.push_back(valuep);
}
}
} else {
for (AstNode* valuep = bin.binp->rangesp(); valuep; valuep = valuep->nextp()) {
values.push_back(valuep);
}
}
return values;
}
// The values of the element-th bin of a run, sign-extended to 'width'
static CrossValueRange runBinRange(AstNode* nodep, const BinRun& run, uint32_t element,
int width) {
const int runw = run.m_lo.width();
V3Number offset{nodep, runw};
offset.opMul(run.m_stride, V3Number{nodep, runw, element});
V3Number lo{nodep, runw};
lo.opAdd(run.m_lo, offset);
V3Number hi = run.m_hi;
if (element + 1 < run.m_count) {
offset.opSub(run.m_stride, V3Number{nodep, runw, 1});
hi.opAdd(lo, offset);
}
CrossValueRange range{nodep, width};
range.lo.opExtendS(lo, runw);
range.hi.opExtendS(hi, runw);
return range;
}
static int crossRangeWidth(AstNode* nodep) {
if (const AstInsideRange* const rangep = VN_CAST(nodep, InsideRange)) {
return std::max(rangep->lhsp()->width(), rangep->rhsp()->width());
}
return nodep->width();
}
static bool crossValueLess(const V3Number& lhs, const V3Number& rhs) {
V3Number result{&lhs};
return !result.opLtS(lhs, rhs).isEqZero();
}
// Round a real bound inward to the nearest coverpoint value (IEEE 1800-2023 19.5.7). Sets
// 'empty' if the domain has no value on the bound's side.
static void crossRealBound(const AstConst* constp, AstNodeExpr* exprp, bool upper,
const CrossValueRange& domain, V3Number& result, bool& empty) {
const double value = constp->num().toDouble();
const double bound = upper ? std::floor(value) : std::ceil(value);
// Powers of two are exact, so the integral bound compares exactly with the domain edges.
const double limit
= std::ldexp(1.0, exprp->isSigned() ? exprp->width() - 1 : exprp->width());
const double minimum = exprp->isSigned() ? -limit : 0.0;
if (std::isnan(bound) || (upper ? bound < minimum : bound >= limit)) {
empty = true;
result = domain.lo;
} else if (bound < minimum) {
result = domain.lo;
} else if (bound >= limit) {
result = domain.hi;
} else {
V3Number real{&result, 64};
real.setDouble(bound);
result.opRToIRoundS(real);
}
}
static bool crossRangeBound(AstNode* nodep, AstNodeExpr* exprp, bool upper, bool binValue,
const CrossValueRange& domain, V3Number& result, bool& empty) {
if (VN_IS(nodep, Unbounded)) {
V3Number limit{nodep, exprp->width()};
if (upper) limit.setAllBits1();
if (exprp->isSigned()) {
limit.setBit(exprp->width() - 1, !upper);
result.opExtendS(limit, limit.width());
} else {
result.opAssign(limit);
}
return true;
}
const AstConst* const constp = VN_CAST(nodep, Const);
if (!constp) return false;
if (constp->num().isDouble()) {
crossRealBound(constp, exprp, upper, domain, result, empty);
return true;
}
if (constp->num().isString()) return false;
if (binValue && exprp->isSigned() && constp->width() <= exprp->width()) {
// Bin bit patterns use the coverpoint's effective type (IEEE 1800-2023 19.5.7).
// Wider values and intersect filters retain their values for domain clipping.
V3Number value{nodep, exprp->width()};
if (constp->isSigned()) {
value.opExtendS(constp->num(), constp->width());
} else {
value.opAssign(constp->num());
}
result.opExtendS(value, value.width());
} else if (constp->isSigned()) {
result.opExtendS(constp->num(), constp->width());
} else {
result.opAssign(constp->num());
}
return true;
}
static CrossValueRange crossValueDomain(AstNode* nodep, int valueWidth, bool isSigned,
int width) {
CrossValueRange domain{nodep, width};
V3Number lo{nodep, valueWidth};
V3Number hi{nodep, valueWidth};
hi.setAllBits1();
if (isSigned) {
lo.setBit(valueWidth - 1, 1);
hi.setBit(valueWidth - 1, 0);
domain.lo.opExtendS(lo, valueWidth);
domain.hi.opExtendS(hi, valueWidth);
} else {
domain.lo.opAssign(lo);
domain.hi.opAssign(hi);
}
return domain;
}
static void intersectCrossRange(CrossValueRange& range, const CrossValueRange& other) {
if (crossValueLess(range.lo, other.lo)) range.lo = other.lo;
if (crossValueLess(other.hi, range.hi)) range.hi = other.hi;
}
static bool crossValueRange(AstNode* nodep, AstNodeExpr* exprp, bool binValue, bool wildcard,
const CrossValueRange& domain, CrossValueRange& range) {
bool empty = false;
const AstConst* const constp = VN_CAST(nodep, Const);
if (const AstInsideRange* const rangep = VN_CAST(nodep, InsideRange)) {
if (!crossRangeBound(rangep->lhsp(), exprp, false, binValue, domain, range.lo, empty)
|| !crossRangeBound(rangep->rhsp(), exprp, true, binValue, domain, range.hi, empty)
|| range.lo.isFourState() || range.hi.isFourState()) {
return false;
}
} else if (constp && constp->num().isDouble()) {
// A real value participates only if integral; it has no wildcard bits.
crossRealBound(constp, exprp, false, domain, range.lo, empty);
crossRealBound(constp, exprp, true, domain, range.hi, empty);
} else {
if (!crossRangeBound(nodep, exprp, false, binValue, domain, range.lo, empty)) {
return false;
}
if (binValue && exprp->isSigned() && constp && !constp->isSigned()
&& constp->width() > exprp->width()) {
bool representable = true;
for (int bit = exprp->width(); bit < constp->width(); ++bit) {
representable &= !constp->num().bitIs1(bit);
}
if (representable) {
V3Number value{nodep, exprp->width()};
value.opAssign(constp->num());
range.lo.opExtendS(value, value.width());
}
}
range.hi = range.lo;
range.wildcard = wildcard;
if (wildcard) {
range.pattern = range.lo;
range.lo = domain.lo;
range.hi = domain.hi;
if (constp && constp->isSigned()) {
// Replicated X sign bits are correlated, not independent wildcards.
// The source domain preserves expansion-before-casting (19.5.7).
intersectCrossRange(
range, crossValueDomain(nodep, constp->width(), true, domain.lo.width()));
}
}
}
if (empty) {
range.lo = domain.hi;
range.hi = domain.lo;
return true;
}
if (!range.lo.isFourState()) intersectCrossRange(range, domain);
if (range.wildcard && exprp->isSigned()) {
const int sign = exprp->width() - 1;
if (constp && !constp->isSigned() && constp->width() > exprp->width()) {
// Unsigned equality preserves every target bit pattern if discarded bits are zero.
for (int bit = exprp->width(); bit < constp->width(); ++bit) {
if (constp->num().bitIs1(bit)) {
range.lo = domain.hi;
range.hi = domain.lo;
return true;
}
}
for (int bit = exprp->width(); bit < range.pattern.width(); ++bit) {
if (range.pattern.bitIsXZ(sign))
range.pattern.setBit(bit, 'x');
else
range.pattern.setBit(bit, range.pattern.bitIs1(sign));
}
return true;
}
// Discarded high bits constrain the target sign, rather than becoming don't-cares.
for (int bit = exprp->width(); bit < range.pattern.width(); ++bit) {
if (range.pattern.bitIsXZ(bit)) continue;
const bool value = range.pattern.bitIs1(bit);
if (!range.pattern.bitIsXZ(sign) && range.pattern.bitIs1(sign) != value) {
range.lo = domain.hi;
range.hi = domain.lo;
break;
}
range.pattern.setBit(sign, value);
}
}
return true;
}
enum CrossRangeState : uint8_t {
CROSS_INSIDE_BOUNDS = 0, // Prefix is strictly inside the interval
CROSS_AT_LOWER = 1,
CROSS_AT_UPPER = 2,
CROSS_AT_BOUNDS = CROSS_AT_LOWER | CROSS_AT_UPPER,
CROSS_NO_MATCH = 4 // Prefix cannot match the interval/pattern
};
static CrossRangeState crossRangeStep(const CrossValueRange& range, CrossRangeState state,
int bit, int value) {
if (state == CROSS_NO_MATCH) return CROSS_NO_MATCH;
// Flipping the sign bit makes signed order lexicographic.
const bool sign = bit == range.pattern.width() - 1;
if (!range.pattern.bitIsXZ(bit) && value != (range.pattern.bitIs1(bit) ^ sign)) {
return CROSS_NO_MATCH;
}
const int low = range.lo.bitIs1(bit) ^ sign;
const int high = range.hi.bitIs1(bit) ^ sign;
if (((state & CROSS_AT_LOWER) && value < low)
|| ((state & CROSS_AT_UPPER) && value > high)) {
return CROSS_NO_MATCH;
}
return static_cast<CrossRangeState>(
((state & CROSS_AT_LOWER) && value == low ? CROSS_AT_LOWER : CROSS_INSIDE_BOUNDS)
| ((state & CROSS_AT_UPPER) && value == high ? CROSS_AT_UPPER : CROSS_INSIDE_BOUNDS));
}
static bool crossWildcardIntersects(const CrossValueRange& range) {
unsigned states = 1U << CROSS_AT_BOUNDS;
for (int bit = range.pattern.width() - 1; bit >= 0 && states; --bit) {
unsigned next = 0;
for (const CrossRangeState state :
{CROSS_INSIDE_BOUNDS, CROSS_AT_LOWER, CROSS_AT_UPPER, CROSS_AT_BOUNDS}) {
if (!(states & (1U << state))) continue;
for (int value = 0; value < 2; ++value) {
const CrossRangeState equal = crossRangeStep(range, state, bit, value);
if (equal != CROSS_NO_MATCH) next |= 1U << equal;
}
}
states = next;
}
return states != 0;
}
// The least value at least 'from' with the non-x bits of 'pattern', in unsigned order; false
// if none. A bit scan, which does not enumerate the pattern's x bits.
static bool nextPatternValue(const V3Number& pattern, const V3Number& from, V3Number& result) {
result.opAssign(from);
int carry = -1; // Lowest x bit above the bit scanned where 'from' has a 0
for (int bit = from.width() - 1; bit >= 0; --bit) {
if (pattern.bitIsXZ(bit)) {
if (!from.bitIs1(bit)) carry = bit;
continue;
}
if (pattern.bitIs1(bit) == from.bitIs1(bit)) continue;
if (from.bitIs1(bit)) { // Only a greater prefix, an x bit above raised, can match
if (carry < 0) return false;
bit = carry;
}
// Then the least such value: the pattern's bits, with x bits of zero
result.setBit(bit, 1);
while (--bit >= 0) result.setBit(bit, pattern.bitIs1(bit));
return true;
}
return true;
}
// Append to 'runs' the maximal runs of consecutive values of 'range' that its pattern
// matches, in value order, until there are more than 'limit'. A match continues through
// the pattern's trailing x bits only, which the next value's carry leaves.
static void crossRangeRuns(const CrossValueRange& range, size_t limit,
std::vector<std::pair<V3Number, V3Number>>& runs) {
// Values order signed, which is the unsigned order of values with the sign bit flipped
const int sign = range.lo.width() - 1;
const auto flipped = [sign](V3Number value) {
if (!value.bitIsXZ(sign)) value.setBit(sign, !value.bitIs1(sign));
return value;
};
const V3Number pattern = flipped(range.pattern);
const V3Number hi = flipped(range.hi);
const V3Number one{&hi, hi.width(), 1};
V3Number trailing{&hi, hi.width()};
for (int bit = 0; bit <= sign && pattern.bitIsXZ(bit); ++bit) trailing.setBit(bit, 1);
V3Number from = flipped(range.lo);
V3Number first{&hi, hi.width()};
V3Number last{&hi, hi.width()};
V3Number less{&hi};
while (runs.size() <= limit && nextPatternValue(pattern, from, first)
&& less.opLt(hi, first).isEqZero()) {
last.opOr(first, trailing);
if (!less.opLt(hi, last).isEqZero()) last = hi;
runs.emplace_back(flipped(first), flipped(last));
if (last.isCaseEq(hi)) break;
from.opAdd(last, one);
}
}
// True if no coverpoint value participates in a resolved value or range. A value with x
// or z bits participates only as a wildcard pattern (IEEE 1800-2023 19.5.7).
static bool crossRangeEmpty(const CrossValueRange& range) {
if (range.lo.isFourState()) return true;
return crossValueLess(range.hi, range.lo)
|| (range.wildcard && !crossWildcardIntersects(range));
}
// Comparison width that holds both the coverpoint's and a bin or intersect value's range.
static int resolveWidth(AstNode* nodep, const AstNodeExpr* exprp) {
return std::max(exprp->width(), crossRangeWidth(nodep)) + 1;
}
// Resolve a bin or intersect value to coverpoint values (IEEE 1800-2023 19.5.7), in the
// width 'range' was created with. False if the value is not a constant integral or real.
static bool resolveValue(AstNode* nodep, AstNodeExpr* exprp, bool binValue, bool wildcard,
CrossValueRange& range) {
const CrossValueRange domain
= crossValueDomain(nodep, exprp->width(), exprp->isSigned(), range.lo.width());
return crossValueRange(nodep, exprp, binValue, wildcard, domain, range);
}
static bool crossRangesIntersect(const CrossValueRange& bin, const CrossValueRange& filter) {
// Values with x or z bits do not participate, even in an identical filter.
if (bin.lo.isFourState() || filter.lo.isFourState()) return false;
CrossValueRange match = bin;
intersectCrossRange(match, filter);
return !crossValueLess(match.hi, match.lo)
&& (!bin.wildcard || crossWildcardIntersects(match));
}
static void unsupportedCrossRange(AstCoverBinsof* selectp, bool& valid) {
selectp->v3warn(COVERIGN, "Unsupported: non-constant or non-integral 'intersect' value, "
"or four-state range bound.");
valid = false;
}
static bool crossValueMatchesFilters(AstCoverBinsof* selectp, AstNode* valuep,
AstNodeExpr* exprp, const AstCoverBin* binp,
const CrossValueRange& domain,
const std::vector<CrossValueRange>& filters,
bool& valid) {
CrossValueRange range{valuep, domain.lo.width()};
if (!crossValueRange(valuep, exprp, true, binp->isWildcard(), domain, range)) {
unsupportedCrossRange(selectp, valid);
return false;
}
for (const CrossValueRange& filter : filters) {
if (crossRangesIntersect(range, filter)) return true;
}
return false;
}
std::vector<bool> selectCoverpointBins(AstCoverBinsof* selectp, const CoverpointBins& bins,
uint32_t first, uint32_t count, bool& valid) {
if (selectp->rangesp() && !bins.exprp->dtypep()->skipRefp()->isIntegralOrPacked()) {
unsupportedCrossRange(selectp, valid);
return {};
}
std::vector<bool> selected(bins.total, false);
std::vector<std::vector<AstNode*>> values;
int width = bins.exprp->width();
for (AstNode* rangep = selectp->rangesp(); rangep; rangep = rangep->nextp()) {
width = std::max(width, crossRangeWidth(rangep));
}
if (selectp->rangesp()) {
values.reserve(count);
for (uint32_t i = first; i < first + count; ++i) {
// A run's values are in the coverpoint type, whose width 'width' covers
values.push_back(bins.values[i].runp ? std::vector<AstNode*>{}
: crossBinValues(bins.values[i]));
for (AstNode* const valuep : values.back()) {
width = std::max(width, crossRangeWidth(valuep));
}
}
}
// One extra bit preserves both unsigned maxima and negative signed bounds.
++width;
const CrossValueRange domain
= crossValueDomain(selectp, bins.exprp->width(), bins.exprp->isSigned(), width);
std::vector<CrossValueRange> filters;
for (AstNode* rangep = selectp->rangesp(); rangep; rangep = rangep->nextp()) {
CrossValueRange filter{rangep, width};
if (!crossValueRange(rangep, bins.exprp, false, false, domain, filter)) {
unsupportedCrossRange(selectp, valid);
return {};
}
filters.push_back(std::move(filter));
}
for (uint32_t i = first; i < first + count; ++i) {
if (!selectp->rangesp()) {
selected[i] = true;
continue;
}
if (const BinRun* const runp = bins.values[i].runp) {
const CrossValueRange range
= runBinRange(selectp, *runp, bins.values[i].element, width);
selected[i] = std::any_of(filters.begin(), filters.end(),
[&](const CrossValueRange& filter) {
return crossRangesIntersect(range, filter);
});
continue;
}
for (AstNode* const valuep : values[i - first]) {
selected[i] = crossValueMatchesFilters(
selectp, valuep, bins.exprp, bins.values[i].binp, domain, filters, valid);
if (!valid) return {};
if (selected[i]) break;
}
}
if (selectp->isNegated()) {
for (uint32_t i = 0; i < bins.total; ++i) selected[i] = !selected[i];
}
return selected;
}
// Constructor-time value metadata of one coverpoint, as C++ list entries
struct ValueLists final {
std::vector<std::string> m_ranges; // Bin, then low and high words
std::vector<std::string> m_runs; // First bin, count, then low, span, and high words
std::vector<std::string> m_patterns; // Bin, then value, mask, low, and high words
std::vector<std::string> m_transitions; // Transition bin
};
// Append a value's words, in the coverpoint's width, to a C++ list entry.
static void appendWords(std::string& text, const V3Number& value, const AstNodeExpr* exprp) {
V3Number narrowed{&value, exprp->width()};
narrowed.opAssign(value);
for (int word = 0; word < exprp->widthWords(); ++word) {
text += ", " + cvtToStr(narrowed.edataWord(word)) + "U";
}
}
void collectValueMetadata(ValueLists& lists, AstNodeExpr* exprp, AstCoverBin* binp,
uint32_t index, AstNodeExpr* valuep) {
const std::string bin = cvtToStr(index) + "U";
if (binp->transp()) lists.m_transitions.push_back(bin);
for (AstNode* const sourcep : crossBinValues({binp, valuep})) {
CrossValueRange range{sourcep, resolveWidth(sourcep, exprp)};
if (!resolveValue(sourcep, exprp, true, binp->isWildcard(), range)) {
// Sampling already resolved every state bin value, so only transitions remain.
sourcep->v3warn(E_UNSUPPORTED, "Unsupported: non-integral value in a transition "
"bin of a coverpoint with exclusions.");
continue;
}
if (crossRangeEmpty(range)) continue;
std::string entry = bin;
if (range.wildcard) {
V3Number value{sourcep, exprp->width()};
V3Number mask{sourcep, exprp->width()};
mask.opBitsNonXZ(range.pattern);
value.opBitsOne(range.pattern);
appendWords(entry, value, exprp);
appendWords(entry, mask, exprp);
}
appendWords(entry, range.lo, exprp);
appendWords(entry, range.hi, exprp);
(range.wildcard ? lists.m_patterns : lists.m_ranges).push_back(entry);
}
}
// Describe a run with one entry, from which the runtime computes the values of its bins.
static void collectRunMetadata(ValueLists& lists, AstNodeExpr* exprp, const BinRun& run) {
V3Number span{exprp, run.m_stride.width(), 0};
span.opSub(run.m_stride, V3Number{exprp, run.m_stride.width(), 1});
std::string entry = cvtToStr(run.m_declared) + "U, " + cvtToStr(run.m_count) + "U";
appendWords(entry, run.m_lo, exprp);
appendWords(entry, span, exprp);
appendWords(entry, run.m_hi, exprp);
lists.m_runs.push_back(entry);
}
// Emit one batched metadata list, bounding the size of each call's temporary list.
void emitValueList(FileLine* fl, AstVar* cpVarp, VCMethod method,
const std::vector<std::string>& entries) {
for (size_t first = 0; first < entries.size(); first += VALUE_LIST_ENTRIES) {
const size_t end = std::min(entries.size(), first + VALUE_LIST_ENTRIES);
std::string text = "{" + entries[first];
for (size_t i = first + 1; i < end; ++i) text += ", " + entries[i];
m_constructorp->addStmtsp(
itemCall(fl, cpVarp, method, {ctext(fl, text + "}")})->makeStmt());
}
}
static bool checkCrossRef(const AstCoverCrossRef* refp, const AstCoverCross* crossp) {
if (refp->name() == crossp->name()) return true;
refp->v3error("Cross selection "
<< refp->prettyNameQ() << " may only name its enclosing cross "
<< crossp->prettyNameQ() << " (IEEE 1800-2023 19.6.1.2).");
return false;
}
static bool checkCrossBinName(const AstCoverCrossBin* binp, std::set<std::string>& names) {
if (names.emplace(binp->name()).second) return true;
binp->v3error("Duplicate cross bin " << binp->prettyNameQ()
<< " (IEEE 1800-2023 19.6.1).");
return false;
}
// The span of the implicit automatic bin reported as 'name' ('auto_<i>', see
// createImplicitAutoBins), found without naming each of its bins
static bool implicitAutoBinSpan(const CoverpointBins& bins, const std::string& name,
BinSpan& span) {
const std::string prefix = "auto_";
if (!VString::startsWith(name, prefix)) return false;
const std::string digits = name.substr(prefix.size());
const unsigned long index = std::strtoul(digits.c_str(), nullptr, 10);
// Only the reported spelling names the bin, not e.g. 'auto_01' or 'auto_x'
if (index >= bins.implicitAuto.count || digits != cvtToStr(index)) return false;
const uint32_t offset = static_cast<uint32_t>(index);
span = BinSpan{bins.implicitAuto.first + offset, 1, bins.implicitAuto.declared + offset};
return true;
}
CrossBinsofTarget
resolveBinsofTarget(const AstCoverBinsof* selectp, const AstCoverCross* crossp,
const std::vector<AstVar*>& cpVars,
const std::map<std::string, uint32_t>& dimensions) const {
const auto dim = dimensions.find(selectp->pointp()->name());
if (dim == dimensions.end()) {
selectp->v3error("binsof coverpoint "
<< selectp->pointp()->prettyNameQ() << " is not an item of cross "
<< crossp->prettyNameQ() << " (IEEE 1800-2023 19.6.1).");
return {};
}
const CoverpointBins& bins = m_cpBins.at(cpVars[dim->second]);
CrossBinsofTarget target{&bins, dim->second, 0, bins.total};
if (!selectp->name().empty()) {
const auto bin = bins.spans.find(selectp->name());
BinSpan span{0, 0, 0};
if (bin != bins.spans.end()) {
span = bin->second;
} else if (!implicitAutoBinSpan(bins, selectp->name(), span)) {
selectp->v3error("Cannot find bin " << selectp->prettyNameQ() << " in coverpoint "
<< selectp->pointp()->prettyNameQ()
<< " (IEEE 1800-2023 19.6.1).");
return {};
}
target.m_first = span.first;
target.m_count = span.count;
target.m_declaredFirst = span.declared;
target.m_declaredEnd = span.declared + span.count;
target.m_sized = span.sized;
}
return target;
}
bool generateRuntimeSelection(AstNode* nodep, AstCoverCross* crossp, AstVar* cxp,
const std::vector<AstVar*>& cpVars,
const std::map<string, uint32_t>& dimensions) {
FileLine* const fl = nodep->fileline();
if (const AstCoverCrossRef* const refp = VN_CAST(nodep, CoverCrossRef)) {
if (!checkCrossRef(refp, crossp)) return false;
m_constructorp->addStmtsp(
itemCall(fl, cxp, VCMethod::COVERGROUP_SELECT_ALL)->makeStmt());
return true;
}
if (const AstCoverCrossSelect* const opp = VN_CAST(nodep, CoverCrossSelect)) {
if (!generateRuntimeSelection(opp->lhsp(), crossp, cxp, cpVars, dimensions)
|| !generateRuntimeSelection(opp->rhsp(), crossp, cxp, cpVars, dimensions)) {
return false;
}
m_constructorp->addStmtsp(itemCall(fl, cxp,
opp->isOr() ? VCMethod::COVERGROUP_SELECT_OR
: VCMethod::COVERGROUP_SELECT_AND)
->makeStmt());
return true;
}
AstCoverBinsof* const selectp = VN_AS(nodep, CoverBinsof);
const CrossBinsofTarget target = resolveBinsofTarget(selectp, crossp, cpVars, dimensions);
if (!target.m_binsp) return false;
const CoverpointBins& bins = *target.m_binsp;
if (selectp->rangesp() && !bins.exprp->dtypep()->skipRefp()->isIntegralOrPacked()) {
bool valid = true;
unsupportedCrossRange(selectp, valid);
return false;
}
AstNodeExpr* firstp;
AstNodeExpr* endp;
if (target.m_sized < 0) {
firstp = cnum(fl, target.m_declaredFirst);
endp = cnum(fl, target.m_declaredEnd);
} else { // A sized array, whose bins the coverpoint's construction placed
AstVar* const cpVarp = cpVars[target.m_dimension];
const uint32_t sized = static_cast<uint32_t>(target.m_sized);
firstp = itemCall(fl, cpVarp, VCMethod::COVERGROUP_SIZED_FIRST, {cnum(fl, sized)});
endp = itemCall(fl, cpVarp, VCMethod::COVERGROUP_SIZED_END, {cnum(fl, sized)});
firstp->dtypeSetUInt32();
endp->dtypeSetUInt32();
}
m_constructorp->addStmtsp(
itemCall(fl, cxp, VCMethod::COVERGROUP_SELECT_DIM,
{cnum(fl, target.m_dimension), firstp, endp, cnum(fl, selectp->isNegated()),
cnum(fl, selectp->rangesp() != nullptr)})
->makeStmt());
for (AstNode* rangep = selectp->rangesp(); rangep; rangep = rangep->nextp()) {
CrossValueRange range{rangep, resolveWidth(rangep, bins.exprp)};
if (!resolveValue(rangep, bins.exprp, false, false, range)) {
bool valid = true;
unsupportedCrossRange(selectp, valid);
return false;
}
if (crossRangeEmpty(range)) continue;
m_constructorp->addStmtsp(itemCall(fl, cxp,
bins.exprp->isWide()
? VCMethod::COVERGROUP_SELECT_RANGE_W
: VCMethod::COVERGROUP_SELECT_RANGE,
{newValueConst(fl, range.lo, bins.exprp),
newValueConst(fl, range.hi, bins.exprp)})
->makeStmt());
}
m_constructorp->addStmtsp(
itemCall(fl, cxp, VCMethod::COVERGROUP_SELECT_DIM_END)->makeStmt());
return true;
}
std::vector<AstCoverCrossBin*>
generateRuntimeCrossBins(AstCoverCross* crossp, AstVar* cxp,
const std::vector<AstVar*>& cpVars,
const std::map<string, uint32_t>& dimensions) {
std::vector<AstCoverCrossBin*> bins;
std::set<string> names;
for (AstNode* itemp = crossp->binsp(); itemp; itemp = itemp->nextp()) {
AstCoverCrossBin* const binp = VN_AS(itemp, CoverCrossBin);
if (!checkCrossBinName(binp, names)) continue;
if (!generateRuntimeSelection(binp->selectp(), crossp, cxp, cpVars, dimensions)) {
continue;
}
FileLine* const fl = binp->fileline();
const bool protect = v3Global.opt.protectIds();
m_constructorp->addStmtsp(
itemCall(fl, cxp, VCMethod::COVERGROUP_SELECT_BIN,
{ctext(fl, binp->binsType().binSetEnum()),
ctext(fl, quoted(VIdProtect::protectWordsIf(binp->name(), protect))),
ctext(fl, quoted(VIdProtect::protectIf(fl->filename(), protect))),
cnum(fl, fl->lineno()), cnum(fl, fl->firstColumn()),
cnum(fl, static_cast<uint32_t>(bins.size()))})
->makeStmt());
bins.push_back(binp);
}
m_constructorp->addStmtsp(
itemCall(crossp->fileline(), cxp, VCMethod::COVERGROUP_FINALIZE_BINS)->makeStmt());
return bins;
}
static void setCrossSelectionRange(CrossSelection& selection, uint64_t first, uint64_t end) {
while (first < end) {
const unsigned bit = first % 64;
const unsigned bits = std::min<uint64_t>(64 - bit, end - first);
selection[VL_BITWORD_Q(first)]
|= (bits == 64 ? ~uint64_t{0} : (uint64_t{1} << bits) - 1) << bit;
first += bits;
}
}
CrossSelection crossSelection(AstNode* nodep, CrossSelectionContext& ctx) {
if (const AstCoverCrossRef* const refp = VN_CAST(nodep, CoverCrossRef)) {
if (!checkCrossRef(refp, ctx.crossp)) {
ctx.valid = false;
return {};
}
CrossSelection result(
VL_BITWORD_Q(static_cast<uint64_t>(ctx.tuples) + VL_QUADSIZE - 1), 0);
setCrossSelectionRange(result, 0, ctx.tuples);
return result;
}
if (AstCoverCrossSelect* const opp = VN_CAST(nodep, CoverCrossSelect)) {
CrossSelection lhs = crossSelection(opp->lhsp(), ctx);
const CrossSelection rhs = crossSelection(opp->rhsp(), ctx);
if (!ctx.valid) return {};
for (size_t i = 0; i < lhs.size(); ++i) {
lhs[i] = opp->isOr() ? lhs[i] | rhs[i] : lhs[i] & rhs[i];
}
return lhs;
}
AstCoverBinsof* const selectp = VN_AS(nodep, CoverBinsof);
const CrossBinsofTarget target
= resolveBinsofTarget(selectp, ctx.crossp, ctx.cpVars, ctx.dimensions);
if (!target.m_binsp) {
ctx.valid = false;
return {};
}
const CoverpointBins& bins = *target.m_binsp;
// Coverpoints with sized arrays are runtime points, so feed only runtime crosses
UASSERT_OBJ(target.m_sized < 0, selectp, "Sized bin array selected by a static cross");
const std::vector<bool> selected
= selectCoverpointBins(selectp, bins, target.m_first, target.m_count, ctx.valid);
if (!ctx.valid) return {};
CrossSelection result(VL_BITWORD_Q(static_cast<uint64_t>(ctx.tuples) + VL_QUADSIZE - 1),
0);
const uint64_t stride = ctx.strides[target.m_dimension];
const uint64_t period = stride * bins.total;
for (uint64_t base = 0; base < ctx.tuples; base += period) {
for (uint32_t i = 0; i < bins.total;) {
if (!selected[i]) {
++i;
continue;
}
const uint32_t begin = i++;
while (i < bins.total && selected[i]) ++i;
setCrossSelectionRange(result, base + begin * stride, base + i * stride);
}
}
return result;
}
// Size the Cartesian product of the declared Normal bins, with each dimension's flat-index
// stride. Live runtime bins only shrink it. Warns and returns false if it is too large.
bool crossShape(AstCoverCross* crossp, const std::vector<AstVar*>& cpVars,
std::vector<uint32_t>& strides, uint32_t& tuples) const {
uint64_t product = std::any_of(cpVars.begin(), cpVars.end(),
[this](AstVar* varp) { return !m_cpBins.at(varp).total; })
? 0
: 1;
strides.resize(cpVars.size());
for (size_t d = cpVars.size(); d > 0; --d) {
strides[d - 1] = static_cast<uint32_t>(product);
product *= m_cpBins.at(cpVars[d - 1]).total;
if (product > UINT32_MAX) {
crossp->v3warn(COVERIGN,
"Unsupported: cross coverage with more than 2^32-1 tuples.");
return false;
}
}
tuples = static_cast<uint32_t>(product);
return true;
}
CrossLayout resolveCrossLayout(AstCoverCross* crossp, const std::vector<AstVar*>& cpVars,
const std::map<std::string, uint32_t>& dimensions) {
CrossLayout layout;
CrossSelectionContext ctx{crossp, cpVars, dimensions, 0, {}};
if (!crossShape(crossp, cpVars, ctx.strides, ctx.tuples)) {
layout.valid = false;
return layout;
}
layout.tuples = ctx.tuples;
CrossSelection occupied;
CrossSelection excluded;
std::set<std::string> names;
for (AstNode* itemp = crossp->binsp(); itemp; itemp = itemp->nextp()) {
AstCoverCrossBin* const binp = VN_AS(itemp, CoverCrossBin);
if (!checkCrossBinName(binp, names)) continue;
ctx.valid = true;
CrossSelection selection = crossSelection(binp->selectp(), ctx);
if (!ctx.valid || std::all_of(selection.begin(), selection.end(), [](uint64_t word) {
return word == 0;
})) {
continue;
}
if (occupied.empty()) {
occupied.resize(selection.size(), 0);
excluded.resize(selection.size(), 0);
}
for (size_t i = 0; i < selection.size(); ++i) {
occupied[i] |= selection[i];
if (!binp->binsType().binIsNormal()) excluded[i] |= selection[i];
}
layout.bins.push_back({binp, std::move(selection)});
}
if (!layout.bins.empty()) {
// IEEE 1800-2023 19.6.2/19.6.3: exclusions also remove tuples from named
// bins, independently of declaration order and sampling guards.
for (ResolvedCrossBin& resolved : layout.bins) {
for (size_t i = 0; i < resolved.selection.size(); ++i) {
if (resolved.binp->binsType().binIsNormal()) {
resolved.selection[i] &= ~excluded[i];
}
if (resolved.selection[i]) ++layout.binWords;
}
}
layout.bins.erase(std::remove_if(layout.bins.begin(), layout.bins.end(),
[](const ResolvedCrossBin& resolved) {
return std::all_of(
resolved.selection.begin(),
resolved.selection.end(),
[](uint64_t word) { return word == 0; });
}),
layout.bins.end());
layout.autoBins = layout.tuples;
for (const uint64_t word : occupied) {
layout.autoBins -= static_cast<uint32_t>(std::bitset<VL_QUADSIZE>{word}.count());
}
}
return layout;
}
AstCoverCrossDType* crossDType(FileLine* fl, uint32_t dimensions, const CrossLayout& layout,
bool dynamic = false) {
const uint32_t bins = static_cast<uint32_t>(layout.bins.size());
const CrossShape shape{dimensions, layout.tuples, bins,
layout.autoBins, layout.binWords, dynamic};
AstCoverCrossDType*& typep = m_cxDTypes[shape];
if (!typep) {
typep = new AstCoverCrossDType{
fl, dimensions, layout.tuples, bins, layout.autoBins, layout.binWords, dynamic};
v3Global.rootp()->typeTablep()->addTypesp(typep);
}
return typep;
}
std::vector<AstCoverCrossBin*> generateCrossBins(AstCoverCross* crossp, AstVar* cxVarp,
const CrossLayout& layout) {
std::vector<AstCoverCrossBin*> bins;
for (const ResolvedCrossBin& resolved : layout.bins) {
AstCoverCrossBin* const binp = resolved.binp;
const CrossSelection& selection = resolved.selection;
FileLine* const fl = binp->fileline();
const bool prot = v3Global.opt.protectIds();
std::string mask = "{";
for (size_t i = 0; i < selection.size(); ++i) {
if (i) mask += ", ";
mask += std::to_string(selection[i]) + "ULL";
}
mask += "}";
m_constructorp->addStmtsp(
itemCall(fl, cxVarp, VCMethod::COVERGROUP_ADD_BIN,
{ctext(fl, binp->binsType().binSetEnum()), ctext(fl, mask),
ctext(fl, quoted(VIdProtect::protectWordsIf(binp->name(), prot))),
ctext(fl, quoted(VIdProtect::protectIf(fl->filename(), prot))),
cnum(fl, static_cast<uint32_t>(fl->lineno())),
cnum(fl, static_cast<uint32_t>(fl->firstColumn()))})
->makeStmt());
bins.push_back(binp);
}
if (!bins.empty()) {
m_constructorp->addStmtsp(
itemCall(crossp->fileline(), cxVarp, VCMethod::COVERGROUP_FINALIZE_BINS)
->makeStmt());
}
return bins;
}
// 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). Each explicit bin adds one configuration call.
void generateCross(AstCoverCross* crossp) {
FileLine* const fl = crossp->fileline();
const bool dynamic = m_runtimeCrosses.count(crossp);
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<AstVar*> cpVars;
std::map<std::string, uint32_t> dimensions;
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");
dimensions.emplace(refp->name(), static_cast<uint32_t>(cpVars.size()));
cpVars.push_back(it->second);
VL_DO_DANGLING(pushDeletep(refp->unlinkFrBack()), refp);
itemp = nextp;
}
const int dims = static_cast<int>(cpVars.size());
CrossLayout layout;
if (dynamic) {
// The runtime sizes the layout from live bins; only check the declared bound here.
std::vector<uint32_t> strides;
uint32_t tuples = 0;
layout.valid = crossShape(crossp, cpVars, strides, tuples);
} else {
layout = resolveCrossLayout(crossp, cpVars, dimensions);
}
if (!layout.valid) return;
AstVar* const cxVarp
= new AstVar{fl, VVarType::MEMBER, "__Vcx_" + crossp->name(),
crossDType(fl, static_cast<uint32_t>(dims), layout, dynamic)};
m_covergroupp->addMembersp(cxVarp);
m_constructorp->addStmtsp(makeItemCreate(fl, cxVarp,
dynamic ? VCMethod::COVERGROUP_ADD_CROSS_DYN
: VCMethod::COVERGROUP_ADD_CROSS));
generateItemWeight(fl, cxVarp, crossp->optionsp());
// 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);
m_constructorp->addStmtsp(makeCrossCpsCall(
fl, cpVars,
itemCall(fl, cxVarp, VCMethod::COVERGROUP_INIT,
{ctext(fl, quoted(hier)), cnum(fl, static_cast<uint32_t>(dims)),
ctext(fl, "__Vcx_cps"),
ctext(fl, quoted(VIdProtect::protectIf(fl->filename(), prot))),
cnum(fl, static_cast<uint32_t>(fl->lineno())),
cnum(fl, static_cast<uint32_t>(fl->firstColumn()))})));
const std::vector<AstCoverCrossBin*> bins
= dynamic ? generateRuntimeCrossBins(crossp, cxVarp, cpVars, dimensions)
: generateCrossBins(crossp, cxVarp, layout);
if (v3Global.opt.coverage()) {
const std::string page
= VIdProtect::protectIf("v_covergroup/" + m_covergroupp->name(), prot);
m_constructorp->addStmtsp(itemCall(fl, cxVarp, VCMethod::COVERGROUP_REGISTER_BINS,
{ctext(fl, "vlSymsp->_vm_contextp__->coveragep()"),
ctext(fl, quoted(page)),
cnum(fl, itemDatabaseWeight(crossp->optionsp())),
cnum(fl, m_cgTypeWeight)})
->makeStmt());
}
// sample(): after all coverpoints have sampled (cross loop runs after coverpoint loop).
UASSERT_OBJ(m_sampleFuncp, crossp, "sample() CFunc not set for cross");
// The cross remembers its feeding coverpoints, so sample() needs no cps array;
// per-bin iff guards still need a temporary array, hence the block form.
const bool hasIffs
= std::any_of(bins.begin(), bins.end(),
[](const AstCoverCrossBin* binp) { return binp->iffp() != nullptr; });
AstNodeStmt* const samplep
= !hasIffs ? static_cast<AstNodeStmt*>(
itemCall(fl, cxVarp, VCMethod::COVERGROUP_SAMPLE)->makeStmt())
: static_cast<AstNodeStmt*>(makeCrossIffsCall(
fl, bins,
itemCall(fl, cxVarp, VCMethod::COVERGROUP_SAMPLE_IFFS,
{ctext(fl, "__Vcx_iffs")})));
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;
// Integral values resolve to the coverpoint's type, as its runtime metadata does
const bool integral = exprp->dtypep()->skipRefp()->isIntegralOrPacked();
// No value form of a wildcard bin is allowed on a real coverpoint (IEEE 1800-2023 19.5.4)
if (binp->isWildcard() && !integral) return wildcardTypeError(binp, exprp);
// Build condition by OR-ing all ranges together
AstNodeExpr* fullCondp = nullptr;
for (AstNode* currRangep = rangep; currRangep; currRangep = currRangep->nextp()) {
AstNodeExpr* rangeCondp = nullptr;
currRangep = V3Const::constifyEdit(currRangep);
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 (IEEE 1800-2023 19.5)");
if (fullCondp) VL_DO_DANGLING(pushDeletep(fullCondp), fullCondp);
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");
if (fullCondp) VL_DO_DANGLING(pushDeletep(fullCondp), fullCondp);
return nullptr;
} else if (integral) {
rangeCondp = buildValueCondition(binp, exprp, irp);
} 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 (IEEE 1800-2023 19.5)");
if (fullCondp) VL_DO_DANGLING(pushDeletep(fullCondp), fullCondp);
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");
if (fullCondp) VL_DO_DANGLING(pushDeletep(fullCondp), fullCondp);
return nullptr;
} else if (integral) {
rangeCondp = buildValueCondition(binp, exprp, irp);
} else {
rangeCondp = makeRangeCondition(irp->fileline(), exprp, minExprp, maxExprp);
}
} else if (AstConst* constp = VN_CAST(currRangep, Const)) {
rangeCondp = buildValueCondition(binp, exprp, constp);
} else {
currRangep->v3error("Non-constant expression in bin range; values must be "
"constants (IEEE 1800-2023 19.5)");
if (fullCondp) VL_DO_DANGLING(pushDeletep(fullCondp), fullCondp);
return nullptr;
}
UASSERT_OBJ(rangeCondp, binp, "rangeCondp is null after building range condition");
fullCondp
= fullCondp ? new AstOr{binp->fileline(), fullCondp, rangeCondp} : rangeCondp;
}
return fullCondp;
}
// Wildcard bits have no meaning for a non-integral coverpoint.
static AstNodeExpr* wildcardTypeError(AstCoverBin* binp, AstNodeExpr* exprp) {
const AstNodeDType* const dtypep = exprp->dtypep()->skipRefp();
exprp->v3error("Cannot use a wildcard bin on a coverpoint of type "
<< dtypep->prettyDTypeNameQ() << " (IEEE 1800-2023 19.5.4).\n"
<< exprp->warnContextPrimary() << '\n'
<< binp->warnOther() << "... Location of wildcard bin\n"
<< binp->warnContextSecondary());
return new AstConst{binp->fileline(), AstConst::BitFalse{}};
}
// Match one bin value, range, or wildcard pattern. Integral coverpoints first resolve the
// value to their type (IEEE 1800-2023 19.5.7). Non-owning: clones what it uses.
AstNodeExpr* buildValueCondition(AstCoverBin* binp, AstNodeExpr* exprp, AstNode* valuep) {
FileLine* const fl = valuep->fileline();
if (!exprp->dtypep()->skipRefp()->isIntegralOrPacked()) {
return new AstEq{fl, exprp->cloneTree(false),
VN_AS(valuep, NodeExpr)->cloneTree(false)};
}
CrossValueRange range{valuep, resolveWidth(valuep, exprp)};
if (!resolveValue(valuep, exprp, true, binp->isWildcard(), range)) {
valuep->v3warn(E_UNSUPPORTED,
"Unsupported: non-integral value in a coverage bin of an "
"integral coverpoint.");
return new AstConst{fl, AstConst::BitFalse{}};
}
if (crossRangeEmpty(range)) return new AstConst{fl, AstConst::BitFalse{}};
AstConst* const lop = newValueConst(fl, range.lo, exprp);
AstConst* const hip = newValueConst(fl, range.hi, exprp);
AstNodeExpr* condp = nullptr;
if (lop->num().isCaseEq(hip->num())) {
condp = new AstEq{fl, exprp->cloneTree(false), lop};
} else {
condp = makeRangeCondition(fl, exprp, lop, hip);
VL_DO_DANGLING(pushDeletep(lop), lop);
}
VL_DO_DANGLING(pushDeletep(hip), hip);
if (!range.wildcard) return condp;
// Match the pattern's value bits within the source-value bounds.
V3Number mask{valuep, exprp->width()};
V3Number value{valuep, exprp->width()};
mask.opBitsNonXZ(range.pattern);
value.opBitsOne(range.pattern);
AstConst* const maskConstp = newValueConst(fl, mask, exprp);
AstConst* const valueConstp = newValueConst(fl, value, exprp);
AstNodeExpr* const exprMasked = new AstAnd{fl, exprp->cloneTree(false), maskConstp};
AstNodeExpr* const valueMasked = new AstAnd{fl, valueConstp, maskConstp->cloneTree(false)};
return new AstLogAnd{fl, condp, 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();
// get_inst_coverage(): the average of the coverpoints and crosses, weighted by their
// option.weight (IEEE 1800-2023 19.11). The instance node holds their runtimes.
AstFunc* const getInstCoveragep
= VN_AS(m_memberMap.findMember(m_covergroupp, "get_inst_coverage"), Func);
FileLine* const instFl = getInstCoveragep->fileline();
AstCMethodHard* const instCallp = instanceCall(instFl, VCMethod::COVERGROUP_COVERAGE);
instCallp->dtypeSetDouble();
getInstCoveragep->addStmtsp(new AstAssign{
instFl, new AstVarRef{instFl, VN_AS(getInstCoveragep->fvarp(), Var), VAccess::WRITE},
instCallp});
// get_coverage(): the average of the covergroup's instances, weighted by their
// option.weight (IEEE 1800-2023 19.11.3). Static, so the registry finds the instances.
AstFunc* const getCoveragep
= VN_AS(m_memberMap.findMember(m_covergroupp, "get_coverage"), Func);
FileLine* const typeFl = getCoveragep->fileline();
AstCExpr* const registryp
= ctext(typeFl, "vlSymsp->_vm_contextp__->covergroupRegistryp()");
registryp->dtypeSetVoid(); // Opaque receiver; only ever the 'fromp' of the call below
AstCMethodHard* const typeCallp
= new AstCMethodHard{typeFl, registryp, VCMethod::COVERGROUP_TYPE_COVERAGE};
typeCallp->addPinsp(ctext(typeFl, quoted(covergroupProtectedName())));
typeCallp->addPinsp(newWeightSel(typeFl, optionVar(true), VAccess::READ));
typeCallp->addPinsp(fileLineDebug(m_covergroupp->fileline()));
typeCallp->usePtr(true);
typeCallp->dtypeSetDouble();
getCoveragep->addStmtsp(new AstAssign{
typeFl, new AstVarRef{typeFl, VN_AS(getCoveragep->fvarp(), Var), VAccess::WRITE},
typeCallp});
}
// 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);
}
void rewriteFuncRef(AstFuncRef* refp, AstVar* handleVarp) {
FileLine* const fl = refp->fileline();
AstArg* const argsp = refp->argsp() ? refp->argsp()->unlinkFrBackWithNext() : nullptr;
AstMethodCall* const callp = new AstMethodCall{
fl, new AstVarRef{fl, handleVarp, VAccess::READ}, refp->name(), argsp};
callp->taskp(refp->taskp());
callp->dtypeFrom(refp);
refp->replaceWith(callp);
VL_DO_DANGLING(pushDeletep(refp), refp);
}
// True if funcp is an instance method of the enclosing class or one of its bases
bool isEnclosingInstanceFunc(const AstNodeFTask* funcp) const {
if (!funcp->classMethod() || funcp->isStatic()) return false;
return AstClass::isClassExtendedFrom(m_enclosingClassp, VN_AS(funcp->aboveLoopp(), Class));
}
bool isEmbeddedCovergroupVar(const AstVar* varp) const {
if (!varp || !varp->isClassMember() || varp->isDeclTyped()) 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<AstNodeAssign*> findCovergroupConstructions() {
std::vector<AstNodeAssign*> foundps;
if (!m_embeddedVarp) return foundps;
AstFunc* const enclosingNewp
= VN_CAST(m_memberMap.findMember(m_enclosingClassp, "new"), Func);
if (!enclosingNewp) return foundps;
enclosingNewp->foreach([&](AstNodeAssign* asgnp) {
const AstNew* const newp = VN_CAST(asgnp->rhsp(), New);
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;
}
std::set<const AstVar*> enclosingInstanceVars() const {
std::set<const AstVar*> 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<const AstVar*> 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<AstNodeAssign*>& 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<EmbeddedEventTrigger> collectEmbeddedEventTriggers(AstCovergroup* cgp) {
std::vector<EmbeddedEventTrigger> triggers;
const std::set<const AstVar*> 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<EmbeddedEventTrigger>& triggers,
const std::vector<AstNodeAssign*>& 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<AstNodeAssign*> assignps;
m_enclosingClassp->foreach([&](AstNodeAssign* asgnp) {
if (isEventLvalue(asgnp->lhsp(), trigger)) assignps.push_back(asgnp);
});
if (assignps.empty()) {
trigger.eventFl->v3warn(
COVERIGN, "Unsupported: 'covergroup' clocking event signal has no assignment "
"within the enclosing class; no coverage sampled. 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};
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);
}
// Options not lowered: the covergroup was not processed
for (AstCgOptionAssign* const optp : m_cgOptions) {
VL_DO_DANGLING(pushDeletep(optp->unlinkFrBack()), optp);
}
m_cgOptions.clear();
}
class FormalRefVisitor final : public VNVisitor {
const std::map<const AstVar*, AstVar*>& m_replacements;
void visit(AstVarRef* nodep) override {
const auto it = m_replacements.find(nodep->varp());
if (it == m_replacements.end()) return;
nodep->varp(it->second);
}
void visit(AstNode* nodep) override { iterateChildren(nodep); }
public:
explicit FormalRefVisitor(const std::map<const AstVar*, AstVar*>& replacements)
: m_replacements{replacements} {}
void scan(AstNode* nodep) { iterate(nodep); }
};
void validateCovergroupExpressions() {
std::set<const AstVar*> sampleMembers;
std::set<const AstVar*> constructorRefMembers;
for (AstNode* stmtp = m_constructorp->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
const AstVar* const varp = VN_CAST(stmtp, Var);
if (!varp || !varp->isIO() || (!varp->isRef() && !varp->isConstRef())) continue;
const 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");
constructorRefMembers.insert(varp);
constructorRefMembers.insert(memberp);
}
for (AstNode* stmtp = m_sampleFuncp->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
const AstVar* const varp = VN_CAST(stmtp, Var);
if (!varp || !varp->isIO()) continue;
const AstVar* const memberp
= VN_CAST(m_memberMap.findMember(m_covergroupp, varp->name()), Var);
UASSERT_OBJ(memberp && memberp->isClassMember(), varp,
"Covergroup sample argument missing persistent member");
sampleMembers.insert(memberp);
}
CovergroupExprValidVisitor{sampleMembers, constructorRefMembers}.scan(m_constructorp);
}
void rebindFormalRefs() {
std::map<const AstVar*, AstVar*> replacements;
for (AstNode* stmtp = m_constructorp->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
if (const AstVar* const varp = VN_CAST(stmtp, Var)) {
if (!varp->isIO()) 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);
}
}
size_t expectedBindings = 0;
for (const auto& pair : replacements) {
if (pair.first->isRef() || pair.first->isConstRef()) ++expectedBindings;
}
size_t rewrittenBindings = 0;
for (AstNode* stmtp = m_constructorp->stmtsp(); stmtp;) {
AstNode* const nextp = stmtp->nextp();
if (AstAssign* const assignp = VN_CAST(stmtp, Assign)) {
AstVarRef* const lhsp = VN_CAST(assignp->lhsp(), VarRef);
AstVarRef* const rhsp = VN_CAST(assignp->rhsp(), VarRef);
if (lhsp && rhsp
&& (lhsp->varp()->declDirection() == VDirection::REF
|| lhsp->varp()->declDirection() == VDirection::CONSTREF)) {
const auto it = replacements.find(rhsp->varp());
UASSERT_OBJ(it != replacements.end() && it->second == lhsp->varp(), assignp,
"Unexpected covergroup reference binding assignment");
AstCExpr* const bindp = new AstCExpr{assignp->fileline(), ""};
bindp->add(lhsp->unlinkFrBack());
bindp->add(" = &");
bindp->add(rhsp->unlinkFrBack());
assignp->replaceWith(bindp->makeStmt());
VL_DO_DANGLING(pushDeletep(assignp), assignp);
++rewrittenBindings;
}
}
stmtp = nextp;
}
UASSERT_OBJ(rewrittenBindings == expectedBindings, m_constructorp,
"Covergroup reference argument missing binding");
FormalRefVisitor visitor{replacements};
for (AstCoverpoint* const cpp : m_coverpoints) visitor.scan(cpp);
for (AstCoverCross* const crossp : m_coverCrosses) visitor.scan(crossp);
}
AstVarRef* installEnclosingBackPointer(const std::vector<AstNodeAssign*>& 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 member references through it, and pass it into the constructor so
// coverage initialization can read enclosing members. 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<const AstVar*> ownVars;
for (AstNode* itemp = m_covergroupp->membersp(); itemp; itemp = itemp->nextp()) {
if (const AstVar* const varp = VN_CAST(itemp, Var)) ownVars.insert(varp);
}
const std::set<const AstVar*> enclosingVars = enclosingInstanceVars();
std::vector<AstVarRef*> refsToRewrite;
std::vector<AstThisRef*> thisRefsToRewrite;
std::vector<AstFuncRef*> funcRefsToRewrite;
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;
}
});
rootp->foreach([&](AstFuncRef* refp) {
if (!isEnclosingInstanceFunc(refp->taskp())) return;
funcRefsToRewrite.push_back(refp);
if (!offenderp) offenderp = refp;
});
};
for (AstCoverpoint* const cpp : m_coverpoints) scan(cpp);
for (AstCoverCross* const crossp : m_coverCrosses) scan(crossp);
for (AstCgOptionAssign* const optp : m_cgOptions) scan(optp);
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};
m_covergroupp->addMembersp(handleVarp);
AstVar* const argumentp = new AstVar{fl, VVarType::BLOCKTEMP, "__Vcg_parentp", enclDTypep};
argumentp->direction(VDirection::INPUT);
argumentp->declDirection(VDirection::INPUT);
argumentp->funcLocal(true);
argumentp->noReset(true);
argumentp->lifetime(VLifetime::AUTOMATIC_EXPLICIT);
m_constructorp->addStmtsp(argumentp);
m_constructorp->stmtsp()->addHereThisAsNext(
new AstAssign{fl, memberRef(fl, handleVarp, VAccess::WRITE),
new AstVarRef{fl, argumentp, VAccess::READ}});
// 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); }
for (AstFuncRef* const refp : funcRefsToRewrite) { rewriteFuncRef(refp, handleVarp); }
// Append a named hidden argument to preserve positional and defaulted user arguments.
for (AstNodeAssign* const constructp : constructps) {
FileLine* const cfl = constructp->fileline();
AstCExpr* const thisp = new AstCExpr{cfl, "this"};
thisp->dtypep(enclDTypep);
VN_AS(constructp->rhsp(), New)->addArgsp(new AstArg{cfl, argumentp->name(), 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);
VL_RESTORER_CLEAR(m_cgOptions);
m_covergroupp = nodep;
m_embeddedVarp = findEmbeddedCovergroupVar();
m_sampleFuncp = nullptr;
m_constructorp = nullptr;
std::vector<EmbeddedEventTrigger> 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");
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;
}
// 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"));
// 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);
validateCovergroupExpressions();
rebindFormalRefs();
deleteCoverageItems();
if (embeddedEventForkp) {
VL_DO_DANGLING(pushDeletep(embeddedEventForkp), embeddedEventForkp);
}
return;
}
iterateChildren(nodep);
validateCovergroupExpressions();
rebindFormalRefs();
const std::vector<AstNodeAssign*> 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);
}
// V3Width leaves only the covergroup-level weights, for lowerCovergroupOptions()
void visit(AstCgOptionAssign* nodep) override { m_cgOptions.push_back(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__ << ": ");
if (!CovergroupAssignValidVisitor{nodep}.valid()) V3Error::abortIfErrors();
{ FunctionalCoverageVisitor{nodep}; } // Destruct before checking
V3Global::dumpCheckGlobalTree("coveragefunc", 0, dumpTreeEitherLevel() >= 3);
}