Fix interface localparam dependencies and arbitrary nesting (#6936) (#7128)

This commit is contained in:
em2machine
2026-03-03 06:55:59 -05:00
committed by GitHub
parent ed84f3adb2
commit 5821d0697c
152 changed files with 8247 additions and 562 deletions
+590 -108
View File
@@ -67,6 +67,7 @@
#include <deque>
#include <map>
#include <memory>
#include <unordered_map>
#include <vector>
VL_DEFINE_DEBUG_FUNCTIONS;
@@ -375,6 +376,39 @@ class ParamProcessor final {
key += ",";
}
key += "}";
} else if (const AstClassRefDType* const classRefp = VN_CAST(nodep, ClassRefDType)) {
// For parameterized class types, use the original class name (without specialization
// suffix) plus the actual type parameter values. This ensures equivalent class types
// get the same string representation regardless of which AST node is used.
if (classRefp->classp()) {
const string& className = classRefp->classp()->name();
const string& origName = classRefp->classp()->origName();
const bool isSpecialized = (className != origName);
UINFO(9, "paramValueString ClassRefDType: name="
<< className << " origName=" << origName
<< " isSpecialized=" << isSpecialized
<< " hasParams=" << (classRefp->paramsp() ? "Y" : "N")
<< " classHasGParam=" << classRefp->classp()->hasGParam() << endl);
if (classRefp->paramsp()) {
// ClassRefDType should have been deparameterized (paramsp
// consumed) before cellPinCleanup calls paramValueString.
classRefp->v3fatalSrc( // LCOV_EXCL_LINE
"ClassRefDType still has paramsp in paramValueString");
} else if (isSpecialized) {
// Already specialized class (e.g., c1__Tz1_TBz1) - use full name
// This ensures different specializations are distinguished
key = className;
} else {
// Unspecialized class with no params - use origName
key = origName;
}
} else {
// classp() should always be set; unresolved class refs
// would have errored in LinkDot.
classRefp->v3fatalSrc( // LCOV_EXCL_LINE
"ClassRefDType has null classp in paramValueString");
}
} else if (const AstNodeDType* const dtypep = VN_CAST(nodep, NodeDType)) {
key += dtypep->prettyDTypeName(true);
}
@@ -383,13 +417,24 @@ class ParamProcessor final {
}
string paramValueNumber(AstNode* nodep) {
// TODO: This parameter value number lookup via a constructed key string is not
// particularly robust for type parameters. We should really have a type
// equivalence predicate function.
if (AstRefDType* const refp = VN_CAST(nodep, RefDType)) nodep = refp->skipRefToNonRefp();
// For type parameters (NodeDType), use only the string representation for hashing.
// Using V3Hasher::uncachedHash includes AST node pointer which differs for equivalent
// types represented by different AST nodes (e.g., parameterized class specializations).
// For value parameters, we can still use the AST hash for better collision resistance.
// All call sites resolve through skipRefToNonRefp() or pass non-DType
// nodes, so nodep should never be a bare RefDType here.
if (VN_IS(nodep, RefDType)) { // LCOV_EXCL_LINE
nodep->v3fatalSrc("Unexpected RefDType in paramValueNumber"); // LCOV_EXCL_LINE
}
const string paramStr = paramValueString(nodep);
// cppcheck-suppress unreadVariable
V3Hash hash = V3Hasher::uncachedHash(nodep) + paramStr;
V3Hash hash;
if (VN_IS(nodep, NodeDType)) {
// Type parameter: use only string-based hash for type equivalence
hash = V3Hash{paramStr};
} else {
// Value parameter: use AST hash + string for better collision resistance
hash = V3Hasher::uncachedHash(nodep) + paramStr;
}
// Force hash collisions -- for testing only
// cppcheck-suppress unreadVariable
if (VL_UNLIKELY(v3Global.opt.debugCollision())) hash = V3Hash{paramStr};
@@ -535,7 +580,7 @@ class ParamProcessor final {
// Using map with key=string so that we can scan it in deterministic order
DefaultValueMap params;
for (AstNode* stmtp = modp->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
if (const AstVar* const varp = VN_CAST(stmtp, Var)) {
if (AstVar* const varp = VN_CAST(stmtp, Var)) {
if (varp->isGParam()) {
AstConst* const constp = VN_CAST(varp->valuep(), Const);
// constp can be nullptr if the parameter is not used to instantiate sub
@@ -544,7 +589,8 @@ class ParamProcessor final {
params.emplace(varp->name(), constp);
}
} else if (AstParamTypeDType* const p = VN_CAST(stmtp, ParamTypeDType)) {
params.emplace(p->name(), p->skipRefp());
AstNode* const dtypep = static_cast<AstNode*>(p->skipRefp());
params.emplace(p->name(), dtypep);
}
}
pair.first->second = std::move(params);
@@ -608,6 +654,30 @@ class ParamProcessor final {
} else if (AstClassOrPackageRef* const classRefp = VN_CAST(nodep, ClassOrPackageRef)) {
if (classRefp->classOrPackageSkipp() == oldClassp)
classRefp->classOrPackagep(newClassp);
} else if (AstTypedef* const typedefp = VN_CAST(nodep, Typedef)) {
// Update typedefs that refer to the old class to point to the new class
if (typedefp->subDTypep()) {
if (AstClassRefDType* const classRefp
= VN_CAST(typedefp->subDTypep(), ClassRefDType)) {
if (classRefp->classp() == oldClassp) { classRefp->classp(newClassp); }
}
}
} else if (AstNodeFTaskRef* const ftaskRefp = VN_CAST(nodep, NodeFTaskRef)) {
// Also update FuncRef/TaskRef packagep to point to new class
// This fixes static method calls through typedefs in parameterized classes
if (ftaskRefp->classOrPackagep() == oldClassp) ftaskRefp->classOrPackagep(newClassp);
// Also update taskp if it points to a function in the old class.
// AstNodeFTask::classOrPackagep() (op2) holds a parse-time
// Dot/ClassOrPackageRef for extern declarations, which is deleted
// in LinkDot::moveExternFuncDecl before V3Param runs. For inline
// class methods op2 is nullptr. So this should never match.
if (AstNodeFTask* const oldTaskp = ftaskRefp->taskp()) {
if (oldTaskp->classOrPackagep() == oldClassp) {
oldTaskp->v3fatalSrc( // LCOV_EXCL_LINE
"FTask classOrPackagep unexpectedly matches old class "
<< oldClassp->prettyNameQ());
}
}
}
if (nodep->op1p()) replaceRefsRecurse(nodep->op1p(), oldClassp, newClassp);
@@ -718,6 +788,154 @@ class ParamProcessor final {
void visit(AstNode* nodep) override { iterateChildren(nodep); }
};
// Returns true if entry's cellPath ends with cloneCellp->name() and
// the parent portion of the path resolves (from startModp) to expectModp.
bool cellPathMatchesClone(const string& cellPath, const AstCell* cloneCellp,
AstNodeModule* startModp, const AstNodeModule* expectModp) const {
if (!cloneCellp || cellPath.empty()) return false;
const size_t lastDot = cellPath.rfind('.');
const string lastComp
= (lastDot == string::npos) ? cellPath : cellPath.substr(lastDot + 1);
const size_t braPos = lastComp.find("__BRA__");
const string lastCompBase
= (braPos == string::npos) ? lastComp : lastComp.substr(0, braPos);
if (lastComp != cloneCellp->name() && lastCompBase != cloneCellp->name()) return false;
if (lastDot == string::npos) return true; // No parent portion to verify
const string parentPath = cellPath.substr(0, lastDot);
const AstNodeModule* const resolvedp
= V3LinkDotIfaceCapture::followCellPath(startModp, parentPath);
return resolvedp == expectModp;
}
// Retarget entry.refp (and extraRefps) to the typedef/paramType found
// in targetModp. Returns true if anything was retargeted.
static bool retargetRefToModule(const V3LinkDotIfaceCapture::CapturedEntry& entry,
AstNodeModule* targetModp) {
if (entry.refp->typedefp()) {
if (AstTypedef* const tdp = V3LinkDotIfaceCapture::findTypedefInModule(
targetModp, entry.refp->typedefp()->name())) {
entry.refp->typedefp(tdp);
if (tdp->subDTypep()) {
entry.refp->refDTypep(tdp->subDTypep());
entry.refp->dtypep(tdp->subDTypep());
}
for (AstRefDType* const xrefp : entry.extraRefps) {
xrefp->typedefp(tdp);
if (tdp->subDTypep()) {
xrefp->refDTypep(tdp->subDTypep());
xrefp->dtypep(tdp->subDTypep());
}
}
return true;
}
} else if (entry.paramTypep) {
if (AstParamTypeDType* const ptp = V3LinkDotIfaceCapture::findParamTypeInModule(
targetModp, entry.paramTypep->name())) {
entry.refp->refDTypep(ptp);
entry.refp->dtypep(ptp);
for (AstRefDType* const xrefp : entry.extraRefps) {
xrefp->refDTypep(ptp);
xrefp->dtypep(ptp);
}
return true;
}
}
return false;
}
// Fix cross-module REFDTYPE pointers in newModp after cloneTree.
// Phase A: path-based fixup using ledger entries with cellPath.
// Phase B: reachable-set fallback for remaining REFDTYPEs.
void fixupCrossModuleRefDTypes(AstNodeModule* newModp, AstNodeModule* srcModp,
AstNode* ifErrorp, const IfaceRefRefs& ifaceRefRefs) {
if (!V3LinkDotIfaceCapture::enabled()) return;
// Phase A: path-based fixup using ledger entries
std::set<AstRefDType*> ledgerFixed;
{
const string cloneCP = VN_CAST(ifErrorp, Cell) ? VN_AS(ifErrorp, Cell)->name() : "";
const string srcName = srcModp->name();
UINFO(9, "iface capture FIXUP-A: srcName=" << srcName << " cloneCP='" << cloneCP << "'"
<< endl);
V3LinkDotIfaceCapture::forEach([&](const V3LinkDotIfaceCapture::CapturedEntry& entry) {
if (!entry.refp) return;
if (entry.cloneCellPath != cloneCP) return;
if (!entry.ownerModp || entry.ownerModp->name() != srcName) return;
if (entry.cellPath.empty()) return;
AstRefDType* const refp = entry.refp;
AstNodeModule* const correctModp
= V3LinkDotIfaceCapture::followCellPath(newModp, entry.cellPath);
UINFO(9, " path fixup: " << refp << " cellPath='" << entry.cellPath << "' -> "
<< (correctModp ? correctModp->name() : "<null>")
<< endl);
if (!correctModp || correctModp->dead()) return;
bool fixed = false;
if (refp->typedefp()) {
if (AstTypedef* const newTdp = V3LinkDotIfaceCapture::findTypedefInModule(
correctModp, refp->typedefp()->name())) {
refp->typedefp(newTdp);
fixed = true;
}
}
if (refp->refDTypep()) {
if (AstNodeDType* const newDtp = V3LinkDotIfaceCapture::findDTypeInModule(
correctModp, refp->refDTypep()->name(), refp->refDTypep()->type())) {
refp->refDTypep(newDtp);
fixed = true;
}
}
if (fixed) ledgerFixed.insert(refp);
});
V3Stats::addStatSum("IfaceCapture, Ledger fixups in V3Param", ledgerFixed.size());
}
// Phase B: reachable-set fallback for REFDTYPEs not handled by ledger
std::set<AstNodeModule*> reachable;
reachable.insert(newModp);
std::function<void(AstNodeModule*)> collectReachable;
collectReachable = [&](AstNodeModule* modp) {
for (AstNode* sp = modp->stmtsp(); sp; sp = sp->nextp()) {
if (AstCell* const cellp = VN_CAST(sp, Cell)) {
AstNodeModule* const cellModp = cellp->modp();
if (cellModp && reachable.insert(cellModp).second) {
collectReachable(cellModp);
}
}
}
};
for (const auto& pair : ifaceRefRefs) {
AstIface* const pinIfacep = pair.second->ifaceViaCellp();
if (pinIfacep && reachable.insert(pinIfacep).second) { collectReachable(pinIfacep); }
}
collectReachable(newModp);
// Phase B (reachable-set fallback): Phase A (path-based ledger fixup)
// always resolves all statement-level REFDTYPEs for current tests and
// Aerial. Assert if any REFDTYPE slips through so we can investigate.
// The loop body is assert-only (no mutations); LCOV_EXCL because
// Phase A always resolves everything and ledgerFixed catches all refs.
for (AstNode* stmtp = newModp->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
AstRefDType* const refp = VN_CAST(stmtp, RefDType);
if (!refp) continue;
if (ledgerFixed.count(refp)) continue; // LCOV_EXCL_LINE
// LCOV_EXCL_START
// Check if typedefp or refDTypep points outside the reachable set
auto checkNotStale = [&](const char* label, AstNode* targetp) {
AstNodeModule* const ownerp = V3LinkDotIfaceCapture::findOwnerModule(targetp);
if (!ownerp || ownerp == newModp || VN_IS(ownerp, Package)
|| reachable.count(ownerp))
return; // OK: owner is reachable or self
v3fatalSrc("Phase B reachable-set fallback triggered for "
<< refp->prettyNameQ() << " " << label << " owner="
<< ownerp->prettyNameQ() << " in " << newModp->prettyNameQ());
};
if (refp->typedefp()) checkNotStale("typedefp", refp->typedefp());
if (refp->refDTypep()) checkNotStale("refDTypep", refp->refDTypep());
// LCOV_EXCL_STOP
}
}
// Return true on success, false on error
bool deepCloneModule(AstNodeModule* srcModp, AstNode* ifErrorp, AstPin* paramsp,
const string& newname, const IfaceRefRefs& ifaceRefRefs) {
@@ -731,73 +949,83 @@ class ParamProcessor final {
newModp = srcModp->cloneTree(false);
}
// Mark the source module as a parameterized template now that a specialized
// clone exists. This suppresses width/type errors on the unresolved template
// during widthParamsEdit (which runs before V3LinkDot sets dead()).
srcModp->parameterizedTemplate(true);
// The clone is a specialized instance, not a template. Clear the flag in
// case it was inherited from a prior cloneTree (when srcModp was already
// marked by an earlier specialization).
newModp->parameterizedTemplate(false);
// cloneTree(false) temporarily populates origNode->clonep() for every node under
// srcModp. The capture list still stores those orig AstRefDType* pointers, so walking
// it lets us follow clonep() into newModp and scrub each clone with the saved
// interface context before newModp is re-linked. we have pointers to the same nodes saved
// in the capture map, so we can use them to scrub the new module.
if (V3LinkDotIfaceCapture::enabled()) {
AstCell* const cloneCellp = VN_CAST(ifErrorp, Cell);
UINFO(9, "iface capture clone: " << srcModp->prettyNameQ() << " -> "
<< newModp->prettyNameQ() << endl);
// First pass: register clone entries and direct-retarget
// REFDTYPEs whose owner won't be cloned later.
V3LinkDotIfaceCapture::forEachOwned(
srcModp, [&](const V3LinkDotIfaceCapture::CapturedIfaceTypedef& entry) {
srcModp, [&](const V3LinkDotIfaceCapture::CapturedEntry& entry) {
if (!entry.refp) return;
AstTypedef* const origTypedefp = entry.typedefp;
if (!origTypedefp) return;
// Find the correct typedef from the correct interface clone.
// entry.typedefp points to the original interface's typedef,
// but we need the typedef in the interface clone this module connects to.
AstTypedef* targetTypedefp = nullptr;
const string& typedefName = origTypedefp->name();
for (auto it = ifaceRefRefs.cbegin(); it != ifaceRefRefs.cend(); ++it) {
const AstIfaceRefDType* const portIrefp = it->first;
AstNodeModule* const pinIfacep = it->second->ifaceViaCellp();
if (!pinIfacep) continue;
// If we have a port variable, match against it
if (entry.ifacePortVarp) {
// Get the IfaceRefDType from the captured port variable
AstNodeDType* const portDTypep = entry.ifacePortVarp->subDTypep();
AstIfaceRefDType* entryPortIrefp = VN_CAST(portDTypep, IfaceRefDType);
if (!entryPortIrefp && arraySubDTypep(portDTypep)) {
entryPortIrefp
= VN_CAST(arraySubDTypep(portDTypep), IfaceRefDType);
}
if (entryPortIrefp != portIrefp) continue; // Not the right port
UINFO(9, "iface capture entry: " << entry.refp << " cellPath='"
<< entry.cellPath << "'" << endl);
// Disambiguate via cellPath when cloning the interface
// that owns the typedef (matched via typedefOwnerModName).
if (cloneCellp && entry.ownerModp != srcModp
&& entry.typedefOwnerModName == srcModp->name()) {
UASSERT_OBJ(!entry.cellPath.empty(), entry.refp,
"cellPath is empty in entry matched via typedefOwnerModName");
if (!cellPathMatchesClone(entry.cellPath, cloneCellp, entry.ownerModp,
m_modp)) {
UINFO(9, "iface capture skipping (path mismatch)" << endl);
return;
}
// Search for typedef with same name in the connected interface clone
for (AstNode* stmtp = pinIfacep->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
if (AstTypedef* const tdp = VN_CAST(stmtp, Typedef)) {
if (tdp->name() == typedefName) {
targetTypedefp = tdp;
UINFO(8,
" [iface-capture] found '"
<< typedefName << "' in " << pinIfacep->name()
<< " via port "
<< (entry.ifacePortVarp ? entry.ifacePortVarp->name()
: "<unknown>")
<< endl);
break;
}
}
}
if (targetTypedefp) break;
}
// Fallback to clone of original typedef (existing behavior)
if (!targetTypedefp) targetTypedefp = origTypedefp->clonep();
if (targetTypedefp) {
UINFO(8, " [iface-capture] replaceTypedef "
<< origTypedefp->name() << " -> " << targetTypedefp << endl);
V3LinkDotIfaceCapture::replaceTypedef(entry.refp, targetTypedefp);
}
// Propagate to cloned RefDType in new module
// Register clone entry in ledger (no AST mutation).
if (AstRefDType* const clonedRefp = entry.refp->clonep()) {
V3LinkDotIfaceCapture::propagateClone(entry.refp, clonedRefp);
const string cloneCP = cloneCellp ? cloneCellp->name() : string{};
const V3LinkDotIfaceCapture::TemplateKey tkey{
entry.ownerModp ? entry.ownerModp->name() : "", entry.refp->name(),
entry.cellPath};
V3LinkDotIfaceCapture::propagateClone(tkey, clonedRefp, cloneCP);
} else if (entry.ownerModp != srcModp) {
// REFDTYPE lives in a parent module; clonep() is null.
AstNodeModule* const actualOwnerp
= V3LinkDotIfaceCapture::findOwnerModule(entry.refp);
if (actualOwnerp && actualOwnerp->hasGParam()) return;
// Owner won't be cloned - directly retarget now.
if (retargetRefToModule(entry, newModp)) {
UINFO(9, "iface capture direct retarget: " << entry.refp << " -> "
<< newModp->prettyNameQ()
<< endl);
}
}
});
// Second pass: retarget clone entries (non-empty cloneCellPath)
// whose typedef owner matches the module being cloned.
const string srcName = srcModp->name();
V3LinkDotIfaceCapture::forEach([&](const V3LinkDotIfaceCapture::CapturedEntry& entry) {
if (!entry.refp || entry.cloneCellPath.empty()) return;
if (entry.typedefOwnerModName != srcName) return;
AstNodeModule* const actualOwnerp
= V3LinkDotIfaceCapture::findOwnerModule(entry.refp);
if (!actualOwnerp || actualOwnerp->hasGParam()) return;
if (cloneCellp && !entry.cellPath.empty()
&& !cellPathMatchesClone(entry.cellPath, cloneCellp, actualOwnerp, m_modp)) {
return;
}
if (retargetRefToModule(entry, newModp)) {
UINFO(9, "iface capture clone-entry retarget: "
<< entry.refp << " -> " << newModp->prettyNameQ() << endl);
}
});
}
newModp->name(newname);
@@ -877,6 +1105,11 @@ class ParamProcessor final {
// to find the correct interface for each VarXRef.
if (!ifaceRefRefs.empty()) { VarXRefRelinkVisitor{newModp}; }
// Fix cross-module REFDTYPE pointers in newModp (Phase A path-based
// + Phase B reachable-set fallback).
UASSERT_OBJ(newModp, srcModp, "newModp null before hierarchy fixup");
fixupCrossModuleRefDTypes(newModp, srcModp, ifErrorp, ifaceRefRefs);
// Assign parameters to the constants specified
// DOES clone() so must be finished with module clonep() before here
for (AstPin* pinp = paramsp; pinp; pinp = VN_AS(pinp->nextp(), Pin)) {
@@ -886,7 +1119,7 @@ class ParamProcessor final {
AstConst* const exprp = VN_CAST(newp, Const);
AstConst* const origp = VN_CAST(modvarp->valuep(), Const);
const bool overridden
= !(origp && ParameterizedHierBlocks::areSame(exprp, origp));
= !(origp && exprp && ParameterizedHierBlocks::areSame(exprp, origp));
// Remove any existing parameter
if (modvarp->valuep()) modvarp->valuep()->unlinkFrBack()->deleteTree();
// Set this parameter to value requested by cell
@@ -904,6 +1137,7 @@ class ParamProcessor final {
}
}
}
return true;
}
const ModInfo* moduleFindOrClone(AstNodeModule* srcModp, AstNode* ifErrorp, AstPin* paramsp,
@@ -993,7 +1227,9 @@ class ParamProcessor final {
+= "_" + paramSmallName(srcModp, modvarp) + paramValueNumber(pinp->exprp());
any_overridesr = true;
} else {
UINFO(9, "cellPinCleanup: before constify " << pinp << " " << modvarp);
V3Const::constifyParamsEdit(pinp->exprp());
UINFO(9, "cellPinCleanup: after constify " << pinp);
// String constants are parsed as logic arrays and converted to strings in V3Const.
// At this moment, some constants may have been already converted.
// To correctly compare constants, both should be of the same type,
@@ -1005,6 +1241,9 @@ class ParamProcessor final {
AstConst* const exprp = VN_CAST(pinp->exprp(), Const);
AstConst* const origp = VN_CAST(modvarp->valuep(), Const);
if (!exprp) {
// With DepGraph architecture, all expressions should be constants
// by the time V3Param runs. If not, it's an error.
UINFO(9, "cellPinCleanup: NOT CONST after constify " << pinp);
UINFOTREE(1, pinp, "", "errnode");
pinp->v3error("Can't convert defparam value to constant: Param "
<< pinp->prettyNameQ() << " of " << nodep->prettyNameQ());
@@ -1019,6 +1258,7 @@ class ParamProcessor final {
// Setting parameter to its default value. Just ignore it.
// This prevents making additional modules, and makes coverage more
// obvious as it won't show up under a unique module page name.
UINFO(9, "cellPinCleanup: same as default " << pinp);
} else if (exprp->num().isDouble() || exprp->num().isString()
|| exprp->num().isFourState() || exprp->num().width() != 32) {
longnamer
@@ -1036,9 +1276,37 @@ class ParamProcessor final {
resolveDotToTypedef(pinp->exprp());
AstNodeDType* rawTypep = VN_CAST(pinp->exprp(), NodeDType);
if (rawTypep) V3Width::widthParamsEdit(rawTypep);
// Guard against widthing a struct/union still owned by a
// parameterized template interface (not yet specialized).
// widthParamsEdit is destructive: it evaluates range expressions,
// sets didWidth=1, and removes Range nodes, which would corrupt
// the template's BASICDTYPEs for all subsequent clones.
// Triggered by deeply nested parameterized interfaces (e.g.
// outer_if containing inner_if with struct typedefs) when the
// inner interface hasn't been specialized yet.
bool skipWidthForTemplateStruct = false;
{
// Use non-asserting skip: before widthParamsEdit, type(expr)
// constructs may contain unlinked REFDTYPEs (e.g. type(x-y))
AstNodeDType* const resolvedp = rawTypep ? rawTypep->skipRefOrNullp() : nullptr;
if (resolvedp && (VN_IS(resolvedp, StructDType) || VN_IS(resolvedp, UnionDType))) {
AstNodeModule* const ownerModp
= V3LinkDotIfaceCapture::findOwnerModule(resolvedp);
// Skip if owned by a parameterized template (not yet specialized)
if (ownerModp && ownerModp->parameterizedTemplate()) {
skipWidthForTemplateStruct = true;
V3Stats::addStatSum("Param, Template struct width skips", 1);
UINFO(9, "SKIP-WIDTH-TEMPLATE: struct="
<< resolvedp->prettyTypeName() << " templateOwner="
<< ownerModp->prettyNameQ() << " pin=" << pinp->prettyNameQ()
<< " of " << nodep->prettyNameQ()
<< " srcMod=" << srcModp->prettyNameQ() << endl);
}
}
if (rawTypep && !skipWidthForTemplateStruct) V3Width::widthParamsEdit(rawTypep);
}
AstNodeDType* exprp = rawTypep ? rawTypep->skipRefToNonRefp() : nullptr;
const AstNodeDType* const origp = modvarp->skipRefToNonRefp();
const AstNodeDType* origp = modvarp->skipRefToNonRefp();
if (!exprp) {
pinp->v3error("Parameter type pin value isn't a type: Param "
<< pinp->prettyNameQ() << " of " << nodep->prettyNameQ());
@@ -1047,10 +1315,12 @@ class ParamProcessor final {
<< modvarp->prettyNameQ());
} else {
UINFO(9, "Parameter type assignment expr=" << exprp << " to " << origp);
V3Const::constifyParamsEdit(pinp->exprp()); // Reconcile typedefs
// Constify may have caused pinp->exprp to change
rawTypep = VN_AS(pinp->exprp(), NodeDType);
exprp = rawTypep->skipRefToNonRefp();
if (!skipWidthForTemplateStruct) {
V3Const::constifyParamsEdit(pinp->exprp()); // Reconcile typedefs
// Constify may have caused pinp->exprp to change
rawTypep = VN_AS(pinp->exprp(), NodeDType);
exprp = rawTypep->skipRefToNonRefp();
}
if (!modvarp->fwdType().isNodeCompatible(exprp)) {
pinp->v3error("Parameter type expression type "
<< exprp->prettyDTypeNameQ()
@@ -1062,9 +1332,11 @@ class ParamProcessor final {
// This prevents making additional modules, and makes coverage more
// obvious as it won't show up under a unique module page name.
} else {
VL_DO_DANGLING(V3Const::constifyParamsEdit(exprp), exprp);
rawTypep = VN_CAST(pinp->exprp(), NodeDType);
exprp = rawTypep ? rawTypep->skipRefToNonRefp() : nullptr;
if (!skipWidthForTemplateStruct) {
VL_DO_DANGLING(V3Const::constifyParamsEdit(exprp), exprp);
rawTypep = VN_CAST(pinp->exprp(), NodeDType);
exprp = rawTypep ? rawTypep->skipRefToNonRefp() : nullptr;
}
longnamer += "_" + paramSmallName(srcModp, modvarp) + paramValueNumber(exprp);
any_overridesr = true;
}
@@ -1302,7 +1574,6 @@ class ParamProcessor final {
// Returns new or reused module
// Make sure constification worked
// Must be a separate loop, as constant conversion may have changed some pointers.
// UINFOTREE(1, nodep, "", "cel2");
string longname = srcModp->name() + "_";
if (debug() >= 9 && paramsp) paramsp->dumpTreeAndNext(cout, "- cellparams: ");
@@ -1332,6 +1603,9 @@ class ParamProcessor final {
}
}
UINFO(9, "nodeDeparamCommon: " << srcModp->prettyNameQ() << " overrides=" << any_overrides
<< endl);
AstNodeModule* newModp = nullptr;
if (m_hierBlocks.hierSubRun() && m_hierBlocks.isHierBlock(srcModp->origName())) {
AstNodeModule* const paramedModp
@@ -1343,6 +1617,7 @@ class ParamProcessor final {
newModp = paramedModp;
// any_overrides = true; // Unused later, so not needed
} else if (!any_overrides) {
UINFO(9, "nodeDeparamCommon: no overrides, reusing " << srcModp);
UINFO(8, "Cell parameters all match original values, skipping expansion.");
// If it's the first use of the default instance, create a copy and store it in user3p.
// user3p will also be used to check if the default instance is used.
@@ -1369,10 +1644,8 @@ class ParamProcessor final {
}
const bool cloned = (newModp != srcModp);
UINFO(9, "iface capture module clone src=" << srcModp << " new=" << newModp << " name="
<< newModp->name() << " from cell=" << nodep
<< " cellName=" << nodep->name()
<< " cloned=" << cloned);
UINFO(9, "nodeDeparamCommon result: " << newModp->prettyNameQ() << " cloned=" << cloned
<< endl);
// Link source class to its specialized version for later relinking of method references
if (defaultsResolved) srcModp->user4p(newModp);
@@ -1412,6 +1685,56 @@ class ParamProcessor final {
return newModp;
}
AstNodeModule* ifaceRefDeparam(AstIfaceRefDType* const nodep, AstNodeModule* srcModp) {
// Check for self-reference pattern: typedef iface#(T) this_type inside interface iface
// When processing inside a specialized interface, the IfaceRefDType should point to
// the owner interface, not create an intermediate specialization.
if (m_modp && VN_IS(m_modp, Iface)) {
AstIface* ownerIfacep = const_cast<AstIface*>(VN_AS(m_modp, Iface));
const string ownerOrigName
= ownerIfacep->origName().empty() ? ownerIfacep->name() : ownerIfacep->origName();
const string srcOrigName
= srcModp->origName().empty() ? srcModp->name() : srcModp->origName();
string ownerBaseName = ownerOrigName;
const size_t ownerPos = ownerBaseName.find("__");
if (ownerPos != string::npos) ownerBaseName = ownerBaseName.substr(0, ownerPos);
string srcBaseName = srcOrigName;
const size_t srcPos = srcBaseName.find("__");
if (srcPos != string::npos) srcBaseName = srcBaseName.substr(0, srcPos);
if (ownerBaseName == srcBaseName) {
bool allOwnParams = true;
for (AstPin* pinp = nodep->paramsp(); pinp && allOwnParams;
pinp = VN_AS(pinp->nextp(), Pin)) {
if (AstRefDType* const refp = VN_CAST(pinp->exprp(), RefDType)) {
if (AstParamTypeDType* const ptdp
= VN_CAST(refp->refDTypep(), ParamTypeDType)) {
AstNodeModule* const ptdOwnerp
= V3LinkDotIfaceCapture::findOwnerModule(ptdp);
if (ptdOwnerp != m_modp) allOwnParams = false;
} else {
pinp->v3error( // LCOV_EXCL_LINE
"Self-referencing interface typedef "
"parameter is not a type parameter of "
"the enclosing interface");
}
} else {
pinp->v3error( // LCOV_EXCL_LINE
"Self-referencing interface typedef "
"parameter is not a type reference");
}
}
if (allOwnParams) {
UINFO(5, "ifaceRefDeparam: self-reference pattern detected in "
<< ownerIfacep->prettyNameQ() << ", using owner interface"
<< endl);
V3Stats::addStatSum("Param, Self-reference iface typedefs", 1);
nodep->ifacep(ownerIfacep);
if (nodep->paramsp()) nodep->paramsp()->unlinkFrBackWithNext()->deleteTree();
return ownerIfacep;
}
}
}
AstNodeModule* const newModp
= nodeDeparamCommon(nodep, srcModp, nodep->paramsp(), nullptr, false);
if (!newModp) return nullptr;
@@ -1461,13 +1784,35 @@ public:
// We always run this, even if no parameters, as need to look for interfaces,
// and remove any recursive references
UINFO(4, "De-parameterize: " << nodep);
UINFO(9, "nodeDeparam ENTER node=<"
<< AstNode::nodeAddr(nodep) << ">" << " type=" << nodep->typeName()
<< " srcMod=" << (srcModp ? srcModp->prettyNameQ() : "'<null>'")
<< " srcSomeInstanceName='"
<< (srcModp ? srcModp->someInstanceName() : string("<null>")) << "'"
<< " parentMod=" << (modp ? modp->prettyNameQ() : "'<null>'")
<< " parentSomeInstanceName='"
<< (modp ? modp->someInstanceName() : string("<null>")) << "'"
<< " inputSomeInstanceName='" << someInstanceName << "'" << endl);
// Create new module name with _'s between the constants
UINFOTREE(10, nodep, "", "cell");
// Evaluate all module constants
V3Const::constifyParamsEdit(nodep);
// Set name for warnings for when we param propagate the module
const string instanceName = someInstanceName + "." + nodep->name();
// For AstIfaceRefDType, name() returns the modport name (often empty),
// so use cellName() which is the actual cell instance name.
// If both are empty (interface port, not a cell), skip appending
// to avoid double-dots in the path.
string nodeName = nodep->name();
if (AstIfaceRefDType* const ifaceRefp = VN_CAST(nodep, IfaceRefDType)) {
if (nodeName.empty()) nodeName = ifaceRefp->cellName();
}
const string instanceName
= nodeName.empty() ? someInstanceName : (someInstanceName + "." + nodeName);
srcModp->someInstanceName(instanceName);
UINFO(9, "nodeDeparam SET-SRC-INST srcMod="
<< srcModp->prettyNameQ() << " someInstanceName='"
<< srcModp->someInstanceName() << "'" << " node=<" << AstNode::nodeAddr(nodep)
<< ">" << " nodeType=" << nodep->typeName() << endl);
AstNodeModule* newModp = nullptr;
if (AstCell* const cellp = VN_CAST(nodep, Cell)) {
@@ -1487,6 +1832,15 @@ public:
// Set name for later warnings
newModp->someInstanceName(instanceName);
UINFO(9, "nodeDeparam EXIT node=<"
<< AstNode::nodeAddr(nodep) << ">" << " type=" << nodep->typeName()
<< " srcMod=" << (srcModp ? srcModp->prettyNameQ() : "'<null>'")
<< " srcSomeInstanceName='"
<< (srcModp ? srcModp->someInstanceName() : string("<null>")) << "'"
<< " newMod=" << (newModp ? newModp->prettyNameQ() : "'<null>'")
<< " newSomeInstanceName='"
<< (newModp ? newModp->someInstanceName() : string("<null>")) << "'" << endl);
UINFO(8, " Done with orig " << nodep);
// if (debug() >= 10)
// v3Global.rootp()->dumpTreeFile(v3Global.debugFilename("param-out.tree"));
@@ -1592,6 +1946,12 @@ class ParamVisitor final : public VNVisitor {
// Process once; note user2 will be cleared on specialization, so we will do the
// specialized module if needed
if (!modp->user2SetOnce()) {
UINFO(9, "processWorkQ module begin mod='"
<< modp->name() << "' orig='" << modp->origName() << "'"
<< " someInstanceName='" << modp->someInstanceName() << "'"
<< " hasGParam=" << (modp->hasGParam() ? "yes" : "no")
<< " user3p=" << (modp->user3p() ? "set" : "null")
<< " dead=" << (modp->dead() ? "yes" : "no") << endl);
// TODO: this really should be an assert, but classes and hier_blocks are
// special...
@@ -1643,6 +2003,10 @@ class ParamVisitor final : public VNVisitor {
if (AstNodeModule* const newModp
= m_processor.nodeDeparam(cellp, srcModp, modp, someInstanceName)) {
if (VN_IS(srcModp, Iface)) {
logTemplateLeakRefs(modp, srcModp, "after queued nodeDeparam", cellp);
}
// Add the (now potentially specialized) child module to the work queue
workQueue.emplace(newModp->level(), newModp);
@@ -1662,18 +2026,105 @@ class ParamVisitor final : public VNVisitor {
return dotted.substr(0, dotted.find('.'));
}
// Debug-only diagnostic (requires --debugi-V3Param 9).
// Walks parentModp looking for RefDTypes or VarRefs whose typedef,
// refDType, or variable target is still owned by templateModp (the
// unspecialized interface template). Any such "leak" indicates a
// pointer that was not properly redirected to the clone during
// deparameterization. Logs each leak with ancestry for triage.
void logTemplateLeakRefs(AstNodeModule* parentModp, AstNodeModule* templateModp,
const char* stage, AstNode* contextp) {
if (debug() < 9 || !parentModp || !templateModp || !VN_IS(templateModp, Iface)) return;
// LCOV_EXCL_START // Debug-only diagnostic
int leakCount = 0;
const auto ancestryOf = [](const AstNode* nodep) {
string ancestry;
for (const AstNode* curp = nodep; curp; curp = curp->backp()) {
if (!ancestry.empty()) ancestry += "<-";
ancestry += curp->typeName();
if (VN_IS(curp, NodeModule) || VN_IS(curp, Netlist) || VN_IS(curp, TypeTable)) {
break;
}
}
return ancestry;
};
parentModp->foreach([&](AstRefDType* refp) {
if (refp->typedefp()) {
AstNodeModule* const tdOwnerp
= V3LinkDotIfaceCapture::findOwnerModule(refp->typedefp());
if (tdOwnerp == templateModp) {
++leakCount;
UINFO(9, "TEMPLATE-LEAK "
<< stage << " parent=" << parentModp->prettyNameQ()
<< " template=" << templateModp->prettyNameQ() << " contextType="
<< (contextp ? contextp->typeName() : string("<null>"))
<< " contextName="
<< (contextp ? contextp->prettyNameQ() : "'<null>'")
<< " leak=REFDTYPE typedef owner" << " ref=<"
<< AstNode::nodeAddr(refp) << ">"
<< " refName=" << refp->prettyNameQ()
<< " ancestry=" << ancestryOf(refp) << endl);
}
}
if (refp->refDTypep()) {
AstNodeModule* const rdOwnerp
= V3LinkDotIfaceCapture::findOwnerModule(refp->refDTypep());
if (rdOwnerp == templateModp) {
++leakCount;
UINFO(9, "TEMPLATE-LEAK "
<< stage << " parent=" << parentModp->prettyNameQ()
<< " template=" << templateModp->prettyNameQ() << " contextType="
<< (contextp ? contextp->typeName() : string("<null>"))
<< " contextName="
<< (contextp ? contextp->prettyNameQ() : "'<null>'")
<< " leak=REFDTYPE refDType owner" << " ref=<"
<< AstNode::nodeAddr(refp) << ">"
<< " refName=" << refp->prettyNameQ()
<< " ancestry=" << ancestryOf(refp) << endl);
}
}
});
parentModp->foreach([&](AstVarRef* varrefp) {
if (!varrefp->varp()) return;
AstNodeModule* const varOwnerp
= V3LinkDotIfaceCapture::findOwnerModule(varrefp->varp());
if (varOwnerp != templateModp) return;
++leakCount;
UINFO(9, "TEMPLATE-LEAK "
<< stage << " parent=" << parentModp->prettyNameQ()
<< " template=" << templateModp->prettyNameQ()
<< " contextType=" << (contextp ? contextp->typeName() : string("<null>"))
<< " contextName=" << (contextp ? contextp->prettyNameQ() : "'<null>'")
<< " leak=VARREF target owner" << " ref=<" << AstNode::nodeAddr(varrefp)
<< ">" << " var=" << varrefp->prettyNameQ()
<< " ancestry=" << ancestryOf(varrefp) << endl);
});
if (leakCount > 0) {
UINFO(9, "TEMPLATE-LEAK summary stage='"
<< stage << "' parent=" << parentModp->prettyNameQ() << " template="
<< templateModp->prettyNameQ() << " count=" << leakCount << endl);
}
// LCOV_EXCL_STOP
}
void checkParamNotHier(AstNode* valuep) {
if (!valuep) return;
valuep->foreachAndNext([&](const AstNodeExpr* exprp) {
if (const AstVarXRef* const refp = VN_CAST(exprp, VarXRef)) {
// Allow hierarchical ref to interface params through interface/modport ports
bool isIfacePortRef = false;
// or local interface instances
bool isIfaceRef = false;
if (refp->varp() && refp->varp()->isIfaceParam()) {
const string refname = getRefBaseName(refp);
isIfacePortRef = !refname.empty() && m_ifacePortNames.count(refname);
isIfaceRef
= !refname.empty()
&& (m_ifacePortNames.count(refname) || m_ifaceInstNames.count(refname));
}
if (!isIfacePortRef) {
if (!isIfaceRef) {
refp->v3warn(HIERPARAM, "Parameter values cannot use hierarchical values"
" (IEEE 1800-2023 6.20.2)");
}
@@ -1737,13 +2188,12 @@ class ParamVisitor final : public VNVisitor {
AstCell* const cellp = VN_CAST(nodep, Cell);
if (!cellParamsReferenceIfacePorts(cellp)) {
AstNodeModule* const srcModp = cellp->modp();
if (AstNodeModule* const newModp = m_processor.nodeDeparam(
cellp, srcModp, m_modp, m_modp->someInstanceName())) {
// For specialized interfaces, recursively process nested interface cells.
// This ensures nested interfaces are already specialized when modules
// using the interface are processed (parameter passthrough fix).
if (newModp != srcModp) specializeNestedIfaceCells(newModp);
}
// DISABLED: specializeNestedIfaceCells causes early nested
// iface specialization where PARAMTYPEDTYPE child REFDTYPEs
// point to template structs instead of clone structs,
// destructively widthing the template with default (zero)
// values. See t_interface_nested_struct_param.v.
m_processor.nodeDeparam(cellp, srcModp, m_modp, m_modp->someInstanceName());
}
}
@@ -1756,6 +2206,12 @@ class ParamVisitor final : public VNVisitor {
void visit(AstNodeModule* nodep) override {
if (nodep->recursiveClone()) nodep->dead(true); // Fake, made for recursive elimination
if (nodep->dead()) return; // Marked by LinkDot (and above)
UINFO(9, "V3Param: visit module name="
<< nodep->prettyNameQ() << " orig='" << nodep->origName()
<< "' someInstanceName='" << nodep->someInstanceName()
<< "' hasGParam=" << (nodep->hasGParam() ? "yes" : "no")
<< " user3p=" << (nodep->user3p() ? "set" : "null")
<< " dead=" << (nodep->dead() ? "yes" : "no") << endl);
if (AstClass* const classp = VN_CAST(nodep, Class)) {
if (classp->hasGParam()) {
// Don't enter into a definition.
@@ -1836,8 +2292,22 @@ class ParamVisitor final : public VNVisitor {
if (!nodep->valuep() && !VN_IS(m_modp, Class)) {
nodep->v3error("Parameter without default value is never given value"
<< " (IEEE 1800-2023 6.20.1): " << nodep->prettyNameQ());
} else {
V3Const::constifyParamsEdit(nodep); // The variable, not just the var->init()
} else if (nodep->valuep()) {
// In visit(AstVar*) for localparams, check if expression contains VARXREF
// to another localparam (not parameter). Parameters are already const,
// but localparams may not be evaluated yet.
bool hasVarXRefToLparam = false;
nodep->valuep()->foreach([&](const AstVarXRef* xrefp) {
if (xrefp->varp() && xrefp->varp()->varType() == VVarType::LPARAM) {
hasVarXRefToLparam = true;
}
});
if (hasVarXRefToLparam) {
// Don't constify - let it be evaluated later
return;
}
V3Const::constifyParamsEdit(nodep);
}
}
}
@@ -2127,6 +2597,7 @@ public:
explicit ParamVisitor(ParamState& state, AstNetlist* netlistp)
: m_state{state}
, m_processor{netlistp} {
// Relies on modules already being in top-down-order
iterate(netlistp);
}
@@ -2214,22 +2685,28 @@ public:
netlistp->foreach([](AstNodeFTaskRef* ftaskrefp) {
AstNodeFTask* ftaskp = ftaskrefp->taskp();
if (!ftaskp || !ftaskp->classMethod()) return;
const string funcName = ftaskp->name();
for (AstNode* backp = ftaskrefp->backp(); backp; backp = backp->backp()) {
if (VN_IS(backp, Class)) {
if (backp == ftaskrefp->classOrPackagep())
return; // task is in the same class as reference
break;
string funcName = ftaskp->name();
// Find the nearest containing (ancestor) class for a node.
// Uses aboveLoopp() which correctly skips sibling links
// (e.g. covergroup classes) to find the true parent.
const auto ancestorClassOf = [](AstNode* startp) -> AstClass* {
for (AstNode* np = startp->aboveLoopp(); np; np = np->aboveLoopp()) {
if (AstClass* const cp = VN_CAST(np, Class)) return cp;
}
}
AstClass* classp = nullptr;
for (AstNode* backp = ftaskp->backp(); backp; backp = backp->backp()) {
if (VN_IS(backp, Class)) {
classp = VN_AS(backp, Class);
break;
}
}
return nullptr;
};
AstClass* refClassp = ancestorClassOf(ftaskrefp);
if (refClassp == ftaskrefp->classOrPackagep())
return; // task is in the same class as reference
AstClass* classp = ancestorClassOf(ftaskp);
UASSERT_OBJ(classp, ftaskrefp, "Class method has no class above it");
// If the FUNCREF and its task are both in the same (clone) class but
// classOrPackagep still points to the old template, just retarget it
if (refClassp && refClassp == classp && ftaskrefp->classOrPackagep()
&& ftaskrefp->classOrPackagep() != refClassp) {
ftaskrefp->classOrPackagep(refClassp);
return;
}
if (classp->user3p()) return; // will not get removed, no need to relink
AstClass* const parametrizedClassp = VN_CAST(classp->user4p(), Class);
if (!parametrizedClassp) return;
@@ -2280,11 +2757,11 @@ public:
}
// Set all links pointing to a user3 (deleting) node as null
netlistp->foreach([](AstNode* const nodep) {
nodep->foreachLink([&](AstNode** const linkpp, const char*) {
nodep->foreachLink([&](AstNode** const linkpp, const char* namep) {
if (*linkpp && (*linkpp)->user3()) {
UINFO(9, "clear link " << nodep);
UINFO(9, "clear link " << namep << " on " << nodep);
*linkpp = nullptr;
UINFO(9, "cleared link " << nodep);
UINFO(9, "cleared link " << namep << " on " << nodep);
}
});
});
@@ -2298,6 +2775,11 @@ public:
void V3Param::param(AstNetlist* rootp) {
UINFO(2, __FUNCTION__ << ":");
{ ParamTop{rootp}; } // Destruct before checking
if (dumpTreeEitherLevel() >= 9) V3LinkDotIfaceCapture::dumpEntries("before V3Param");
{ ParamTop{rootp}; }
V3LinkDotIfaceCapture::purgeStaleRefs();
if (dumpTreeEitherLevel() >= 9) V3LinkDotIfaceCapture::dumpEntries("after V3Param");
V3Global::dumpCheckGlobalTree("param", 0, dumpTreeEitherLevel() >= 3);
}