Support covergroups, coverpoints, and bins (#784) (#7117)

Fixes #784.
This commit is contained in:
Matthew Ballance
2026-06-05 09:35:01 -04:00
committed by GitHub
parent 7e2fe64ae2
commit 2886291eba
163 changed files with 7575 additions and 1123 deletions
+263
View File
@@ -64,6 +64,7 @@ class LinkParseVisitor final : public VNVisitor {
AstNodeExpr* m_defaultInSkewp = nullptr; // Current default input skew
AstNodeExpr* m_defaultOutSkewp = nullptr; // Current default output skew
int m_anonUdpId = 0; // Counter for anonymous UDP instances
int m_coverpointNum = 0; // Counter for unnamed coverpoints within current covergroup
int m_genblkAbove = 0; // Begin block number of if/case/for above
int m_genblkNum = 0; // Begin block number, 0=none seen
int m_beginDepth = 0; // How many begin blocks above current node within current AstNodeModule
@@ -1149,6 +1150,268 @@ class LinkParseVisitor final : public VNVisitor {
iterateChildren(nodep);
}
// Append, for each arg in argsp, an INPUT parameter plus a "this.<member> = <param>"
// assignment to funcp. The parameter is a clone of the covergroup member and so shares its
// name; 'this.' on the LHS targets the member, otherwise the same-named local parameter
// shadows it and the assignment self-assigns the parameter, leaving the member unwritten.
// argsp may be null (no args appended).
static void addArgMemberCopies(AstFunc* funcp, AstNode* argsp) {
for (AstNode* argp = argsp; argp; argp = argp->nextp()) {
AstVar* const origVarp = VN_AS(argp, Var);
AstVar* const paramp = origVarp->cloneTree(false);
paramp->funcLocal(true);
paramp->direction(VDirection::INPUT);
funcp->addStmtsp(paramp);
AstNodeExpr* const lhsp = new AstDot{
origVarp->fileline(), false, new AstParseRef{origVarp->fileline(), "this"},
new AstParseRef{origVarp->fileline(), origVarp->name()}};
AstNodeExpr* const rhsp = new AstParseRef{paramp->fileline(), paramp->name()};
funcp->addStmtsp(new AstAssign{origVarp->fileline(), lhsp, rhsp});
}
}
// Create boilerplate covergroup methods on the given AstClass.
// argsp/sampleArgsp are the raw arg lists still owned by the caller; they are iterated
// (cloned) but not deleted here.
static void createCovergroupMethods(AstClass* nodep, AstFunc* newFuncp, AstNode* argsp,
AstNode* sampleArgsp) {
// Hidden static to take unspecified reference argument results
AstVar* const defaultVarp
= new AstVar{nodep->fileline(), VVarType::MEMBER, "__Vint", nodep->findIntDType()};
defaultVarp->lifetime(VLifetime::STATIC_EXPLICIT);
nodep->addStmtsp(defaultVarp);
// Handle constructor arguments - add function parameters and assignments
if (argsp) {
UASSERT_OBJ(newFuncp, nodep,
"Covergroup class must have a 'new' constructor function");
// Save the existing body statements and unlink them, so the arg assignments run
// before the coverage body, then re-append the body.
AstNode* const existingBodyp = newFuncp->stmtsp();
if (existingBodyp) existingBodyp->unlinkFrBackWithNext();
addArgMemberCopies(newFuncp, argsp);
if (existingBodyp) newFuncp->addStmtsp(existingBodyp);
}
// IEEE: option / type_option members allow external access (cg_inst.option.X)
// and require std:: types; std:: is kept because setUsesStdPackage() is called
// at parse time for every covergroup declaration.
{
AstVar* const varp
= new AstVar{nodep->fileline(), VVarType::MEMBER, "option", VFlagChildDType{},
new AstRefDType{nodep->fileline(), "vl_covergroup_options_t",
new AstClassOrPackageRef{nodep->fileline(), "std",
nullptr, nullptr},
nullptr}};
nodep->addMembersp(varp);
}
{
AstVar* const varp
= new AstVar{nodep->fileline(), VVarType::MEMBER, "type_option", VFlagChildDType{},
new AstRefDType{nodep->fileline(), "vl_covergroup_type_options_t",
new AstClassOrPackageRef{nodep->fileline(), "std",
nullptr, nullptr},
nullptr}};
nodep->addMembersp(varp);
}
// IEEE: function void sample([arguments])
{
AstFunc* const funcp = new AstFunc{nodep->fileline(), "sample", nullptr, nullptr};
addArgMemberCopies(funcp, sampleArgsp);
funcp->classMethod(true);
funcp->dtypep(funcp->findVoidDType());
nodep->addMembersp(funcp);
}
// IEEE: function void start(), void stop()
for (const string& name : {"start"s, "stop"s}) {
AstFunc* const funcp = new AstFunc{nodep->fileline(), name, nullptr, nullptr};
funcp->classMethod(true);
funcp->dtypep(funcp->findVoidDType());
nodep->addMembersp(funcp);
}
// IEEE: static function real get_coverage(optional ref int, optional ref int)
// IEEE: function real get_inst_coverage(optional ref int, optional ref int)
for (const string& name : {"get_coverage"s, "get_inst_coverage"s}) {
AstFunc* const funcp = new AstFunc{nodep->fileline(), name, nullptr, nullptr};
funcp->fileline()->warnOff(V3ErrorCode::NORETURN, true);
funcp->isStatic(name == "get_coverage");
funcp->classMethod(true);
funcp->dtypep(funcp->findVoidDType());
nodep->addMembersp(funcp);
{
AstVar* const varp = new AstVar{nodep->fileline(), VVarType::MEMBER, name,
nodep->findDoubleDType()};
varp->lifetime(VLifetime::AUTOMATIC_EXPLICIT);
varp->funcLocal(true);
varp->direction(VDirection::OUTPUT);
varp->funcReturn(true);
funcp->fvarp(varp);
}
for (const string& varname : {"covered_bins"s, "total_bins"s}) {
AstVar* const varp = new AstVar{nodep->fileline(), VVarType::MEMBER, varname,
nodep->findStringDType()};
varp->lifetime(VLifetime::AUTOMATIC_EXPLICIT);
varp->funcLocal(true);
varp->direction(VDirection::INPUT);
varp->valuep(new AstVarRef{nodep->fileline(), defaultVarp, VAccess::READ});
funcp->addStmtsp(varp);
}
}
// IEEE: function void set_inst_name(string)
{
AstFunc* const funcp
= new AstFunc{nodep->fileline(), "set_inst_name", nullptr, nullptr};
funcp->classMethod(true);
funcp->dtypep(funcp->findVoidDType());
nodep->addMembersp(funcp);
AstVar* const varp = new AstVar{nodep->fileline(), VVarType::MEMBER, "name",
nodep->findStringDType()};
varp->lifetime(VLifetime::AUTOMATIC_EXPLICIT);
varp->funcLocal(true);
varp->direction(VDirection::INPUT);
funcp->addStmtsp(varp);
}
}
void visit(AstCovergroup* nodep) override {
// AstCovergroup can only appear inside a module/class/package; never at root level.
UASSERT_OBJ(m_modp, nodep, "AstCovergroup not under module");
// If we're already inside a covergroup class, this is the sentinel AstCovergroup
// node carrying the clocking event for V3Covergroup - don't re-transform it.
if (VN_IS(m_modp, Class) && VN_AS(m_modp, Class)->isCovergroup()) return;
// Transform raw parse-time AstCovergroup into a fully-formed AstClass
cleanFileline(nodep);
const string libname = m_modp->libname();
AstClass* const cgClassp = new AstClass{nodep->fileline(), nodep->name(), libname};
cgClassp->isCovergroup(true);
v3Global.useCovergroup(true);
// Clocking event: unlink before deleteTree, attach as AstCovergroup child on class
if (AstSenTree* const eventp = nodep->eventp()) {
eventp->unlinkFrBack();
AstCovergroup* const cgNodep = new AstCovergroup{
nodep->fileline(), nodep->name(), nullptr, nullptr, nullptr, eventp};
cgClassp->addMembersp(cgNodep);
}
// Convert constructor args to member variables
for (AstNode* argp = nodep->argsp(); argp; argp = argp->nextp()) {
AstVar* const origVarp = VN_AS(argp, Var);
AstVar* const memberp = origVarp->cloneTree(false);
memberp->varType(VVarType::MEMBER);
memberp->funcLocal(false);
memberp->direction(VDirection::NONE);
cgClassp->addMembersp(memberp);
}
// Convert sample args to member variables
for (AstNode* argp = nodep->sampleArgsp(); argp; argp = argp->nextp()) {
AstVar* const origVarp = VN_AS(argp, Var);
AstVar* const memberp = origVarp->cloneTree(false);
memberp->varType(VVarType::MEMBER);
memberp->funcLocal(false);
memberp->direction(VDirection::NONE);
cgClassp->addMembersp(memberp);
}
// Create the constructor; detach membersp (coverage body) and use as its body
AstFunc* const newFuncp = new AstFunc{nodep->fileline(), "new", nullptr, nullptr};
newFuncp->fileline()->warnOff(V3ErrorCode::NORETURN, true);
newFuncp->classMethod(true);
newFuncp->isConstructor(true);
newFuncp->dtypep(cgClassp->dtypep());
if (AstNode* const bodyp = nodep->membersp()) {
bodyp->unlinkFrBackWithNext();
newFuncp->addStmtsp(bodyp);
}
cgClassp->addMembersp(newFuncp);
// Add all boilerplate covergroup methods (reads argsp/sampleArgsp from nodep)
createCovergroupMethods(cgClassp, newFuncp, nodep->argsp(), nodep->sampleArgsp());
// Replace AstCovergroup with AstClass and process the new class normally.
// Reset the unnamed-coverpoint counter so synthesized names are stable and
// independent of unrelated covergroups elsewhere in the file.
VL_RESTORER(m_coverpointNum);
m_coverpointNum = 0;
nodep->replaceWith(cgClassp);
VL_DO_DANGLING(nodep->deleteTree(), nodep);
iterate(cgClassp);
}
void visit(AstCoverpoint* nodep) override {
cleanFileline(nodep);
// Give every coverpoint a guaranteed-unique, deterministic name so all downstream
// consumers (generated bin-variable names, the cross coverpoint map, hierarchical
// report names) see a consistent identifier. Unlabeled coverpoints arrive from the
// grammar with an empty name; left empty, two of them in one covergroup collide on
// the generated "__Vcov__<bin>" variable name (e.g. duplicate "__Vcov__auto_0").
if (nodep->name().empty()) {
// A single-identifier coverpoint expression is the only form that parses to an
// AstParseRef with a usable name here; a dotted/scoped/select/concatenation/call
// expression is either a different node (so the cast yields null) or a name-less
// AstParseRef (e.g. a member-select). Either of those gets a synthesized name.
const AstParseRef* const refp = VN_CAST(nodep->exprp(), ParseRef);
if (refp && !refp->name().empty()) {
// Single-identifier coverpoint: take the variable's name (IEEE 1800-2023
// 19.5 - an unlabeled coverpoint of a single variable is named for it).
nodep->name(refp->name());
} else {
// Compound expression (member/part select, concatenation, ...): synthesize
// a unique name. Leading "__V" keeps it out of the user namespace.
nodep->name("__Vcoverpoint" + cvtToStr(m_coverpointNum++));
}
}
// Re-sort the parse-time mixed bins list (AstCoverBin + AstCgOptionAssign)
// into the typed binsp and optionsp slots. The grammar attaches both node types
// to binsp (op2) as a raw List[AstNode]; now that they are properly parented we
// can iterate and split them without any temporary-parent tricks.
for (AstNode *itemp = nodep->binsp(), *nextp; itemp; itemp = nextp) {
nextp = itemp->nextp();
if (AstCgOptionAssign* const optp = VN_CAST(itemp, CgOptionAssign)) {
optp->unlinkFrBack();
if (optp->optionType() == VCoverOptionType::AT_LEAST
|| optp->optionType() == VCoverOptionType::AUTO_BIN_MAX) {
nodep->addOptionsp(new AstCoverOption{optp->fileline(), optp->optionType(),
optp->valuep()->cloneTree(false)});
} else {
optp->v3warn(COVERIGN,
"Ignoring unsupported coverage option: " + optp->prettyNameQ());
}
VL_DO_DANGLING(optp->deleteTree(), optp);
}
}
iterateChildren(nodep);
}
void visit(AstCoverCross* nodep) override {
cleanFileline(nodep);
// Distribute the parse-time raw cross_body list (rawBodyp, op3) into the
// typed optionsp slot. The grammar produces AstCgOptionAssign nodes for
// option.* items; convert them to AstCoverOption exactly as visit(AstCoverpoint*)
// does. Other items (functions, unsupported bin selectors) are discarded.
for (AstNode *itemp = nodep->rawBodyp(), *nextp; itemp; itemp = nextp) {
nextp = itemp->nextp();
itemp->unlinkFrBack();
AstCgOptionAssign* const optp = VN_AS(itemp, CgOptionAssign);
const VCoverOptionType optType = optp->optionType();
optp->v3warn(COVERIGN,
"Ignoring unsupported coverage cross option: " + optp->prettyNameQ());
// Always preserve the option node so V3Coverage can track its source line
// for coverage annotation, even when the option itself is unsupported.
nodep->addOptionsp(
new AstCoverOption{optp->fileline(), optType, optp->valuep()->cloneTree(false)});
VL_DO_DANGLING(optp->deleteTree(), optp);
}
iterateChildren(nodep);
}
void visit(AstNode* nodep) override {
// Default: Just iterate
cleanFileline(nodep);