diff --git a/src/V3LinkDotIfaceCapture.cpp b/src/V3LinkDotIfaceCapture.cpp index 8cde2845b..28c7ea02b 100644 --- a/src/V3LinkDotIfaceCapture.cpp +++ b/src/V3LinkDotIfaceCapture.cpp @@ -36,14 +36,14 @@ // 3. TARGET RESOLUTION (finalizeIfaceCapture, after V3Param): // Runs after all cloning is complete and cell pointers are wired // to the correct interface clones. For each entry, walks cellPath -// starting from the entry's owner module (using findOwnerModule(refp) -// for clone entries) to find the correct target module, then locates -// the PARAMTYPEDTYPE / TYPEDEF by name and applies it to the REFDTYPE. +// starting from the stored owner module to find the correct target, +// then uses targetKind and the captured name to apply the replacement +// without inspecting the REFDTYPE's inherited target pointers. // ** This is the ONLY place that resolves targets and mutates AST. ** // // KEY INVARIANT: The path {ownerModName, refName, cellPath, cloneCellPath} -// is the sole identity. No clonep(), no pointer matching. The path IS -// the disambiguation. +// plus targetKind is the stable identity. Inherited target pointers are +// not used for final resolution. // // Template entries have cloneCellPath = ""; clone entries get it set by // propagateClone. TemplateKey (ownerModName, refName, cellPath) matches @@ -87,7 +87,6 @@ void V3LinkDotIfaceCapture::reset() { namespace { struct StmtNameMap final { std::unordered_map> m_byName; - std::unordered_map> m_byPrettyName; }; std::unordered_map s_moduleCache; @@ -98,10 +97,6 @@ const StmtNameMap& getOrBuild(AstNodeModule* modp) { for (AstNode* stmtp = modp->stmtsp(); stmtp; stmtp = stmtp->nextp()) { const string& nm = stmtp->name(); if (!nm.empty()) cache.m_byName[nm].push_back(stmtp); - if (AstNodeDType* const dtp = VN_CAST(stmtp, NodeDType)) { - const string pn = dtp->prettyName(); - if (!pn.empty()) cache.m_byPrettyName[pn].push_back(dtp); - } } return cache; } @@ -187,12 +182,40 @@ AstParamTypeDType* V3LinkDotIfaceCapture::findParamTypeInModule(AstNodeModule* m return resultp; } -AstNodeDType* V3LinkDotIfaceCapture::findDTypeByPrettyName(AstNodeModule* modp, - const string& prettyName) { - const StmtNameMap& cache = getOrBuild(modp); - const auto it = cache.m_byPrettyName.find(prettyName); - if (it == cache.m_byPrettyName.end()) return nullptr; - return it->second.front(); +bool V3LinkDotIfaceCapture::retargetRefToModule(const CapturedEntry& entry, + AstNodeModule* targetModp) { + if (!entry.refp || !targetModp) return false; + + if (entry.targetKind == TargetKind::PARAM_TYPE) { + AstParamTypeDType* const paramTypep + = findParamTypeInModule(targetModp, entry.refp->name()); + if (!paramTypep) return false; + const auto retarget = [&](AstRefDType* refp) { + if (!refp) return; + refp->refDTypep(paramTypep); + refp->dtypep(paramTypep); + }; + retarget(entry.refp); + for (AstRefDType* const refp : entry.extraRefps) retarget(refp); + return true; + } + + AstTypedef* const typedefp = findTypedefInModule(targetModp, entry.refp->name()); + if (!typedefp) return false; + AstNodeDType* const dtypep = typedefp->subDTypep(); + const auto retarget = [&](AstRefDType* refp) { + if (!refp) return; + refp->typedefp(typedefp); + // An incomplete typedef is still a successful name resolution, but + // must not erase type links that a later width pass can complete. + if (dtypep) { + refp->refDTypep(dtypep); + refp->dtypep(dtypep); + } + }; + retarget(entry.refp); + for (AstRefDType* const refp : entry.extraRefps) retarget(refp); + return true; } AstNodeModule* V3LinkDotIfaceCapture::findCloneViaHierarchy(AstNodeModule* containingModp, @@ -218,18 +241,60 @@ AstNodeModule* V3LinkDotIfaceCapture::findCloneViaHierarchy(AstNodeModule* conta return nullptr; } +namespace { +using LiveNodes = std::unordered_set; + +// A scoped snapshot of every node currently in the tree. V3Broken::isLinkable() +// cannot serve this role: its table is populated only while V3Broken::brokenAll() +// runs and is cleared before it returns, and brokenAll() would itself assert on +// the dangling cross-links this pass has yet to repair. +LiveNodes collectLiveNodes() { + LiveNodes liveNodes; + v3Global.rootp()->foreach([&](AstNode* nodep) { liveNodes.insert(nodep); }); + return liveNodes; +} + +// A live snapshot, when supplied, stops the walk at the first stale back link; +// callers without one fall back to the sentinel guard below. +AstNodeModule* findOwnerModuleImpl(AstNode* nodep, const LiveNodes* liveNodesp) { + for (AstNode* curp = nodep; curp; curp = curp->backp()) { + if (liveNodesp) { + if (!liveNodesp->count(curp)) return nullptr; + } else if (reinterpret_cast(curp) < 0x1000) { + // Legacy callers lack a liveness snapshot; retain the existing guard + // against sentinel values encountered in corrupted backp() chains. + // It cannot prove an arbitrary freed pointer safe - invalidating + // ledger entries at deletion time would make it unnecessary. + return nullptr; + } + if (AstNodeModule* const modp = VN_CAST(curp, NodeModule)) return modp; + } + return nullptr; +} + +AstNodeModule* findOwnerModuleIfLive(AstNode* nodep, const LiveNodes& liveNodes) { + return findOwnerModuleImpl(nodep, &liveNodes); +} + +bool moduleMatchesOwner(const AstNodeModule* modp, const string& ownerName) { + if (!modp || ownerName.empty()) return false; + return modp->name() == ownerName || modp->origName() == ownerName; +} +} // namespace + int V3LinkDotIfaceCapture::fixDeadRefs(AstRefDType* refp, AstNodeModule* containingModp, - const char* location) { + const char* location, const LiveNodes& liveNodes) { int fixed = 0; // Fix typedefp pointing to dead module - if (refp->typedefp()) { - AstNodeModule* const typedefModp = findOwnerModule(refp->typedefp()); + AstTypedef* const oldTypedefp = refp->typedefp(); + if (oldTypedefp && liveNodes.count(oldTypedefp)) { + AstNodeModule* const typedefModp = findOwnerModuleIfLive(oldTypedefp, liveNodes); if (typedefModp && typedefModp->dead()) { AstNodeModule* cloneModp = nullptr; if (containingModp) { cloneModp = findCloneViaHierarchy(containingModp, typedefModp); } if (cloneModp) { - const string& tdName = refp->typedefp()->name(); + const string& tdName = oldTypedefp->name(); if (AstTypedef* const newTdp = findTypedefInModule(cloneModp, tdName)) { UINFO(9, "iface capture finalizeCapture (" << location << "): fixing typedefp refp=" << refp << " dead=" @@ -241,75 +306,24 @@ int V3LinkDotIfaceCapture::fixDeadRefs(AstRefDType* refp, AstNodeModule* contain } } - // Fix refDTypep pointing to dead module - if (refp->refDTypep()) { - AstNodeModule* const targetModp = findOwnerModule(refp->refDTypep()); - if (targetModp && targetModp->dead()) { - AstNodeModule* cloneModp = nullptr; - if (containingModp) { cloneModp = findCloneViaHierarchy(containingModp, targetModp); } - bool foundByName = false; - if (cloneModp) { - const string& targetName = refp->refDTypep()->prettyName(); - if (AstNodeDType* const newDtp = findDTypeByPrettyName(cloneModp, targetName)) { - UINFO(9, "iface capture finalizeCapture (" - << location << "): fixing refDTypep refp=" << refp - << " dead=" << targetModp->name() << " -> " << cloneModp->name()); - refp->refDTypep(newDtp); - ++fixed; - foundByName = true; - } - } - // If name-based search failed, try to derive refDTypep from - // the already-fixed typedefp chain. The typedefp was fixed - // above to point to the clone's typedef, so its subDTypep() - // returns a live dtype (type-table entry or clone-owned). - // This avoids setting refDTypep to nullptr which would force - // V3Width to re-walk the dtype tree under TYPETABLE where - // module provenance is lost, triggering spurious warnings. - if (!foundByName) { - AstNodeDType* derivedp = nullptr; - if (refp->typedefp() && refp->typedefp()->subDTypep()) { - derivedp = refp->typedefp()->subDTypep(); - AstNodeModule* const derivedOwnerp = findOwnerModule(derivedp); - if (derivedOwnerp && derivedOwnerp->dead()) { derivedp = nullptr; } - } - UINFO(9, "iface capture finalizeCapture (" - << location << "): deriving refDTypep from typedefp refp=" << refp - << " dead=" << targetModp->name() << " derived=" << derivedp); - refp->refDTypep(derivedp); - ++fixed; - } - } + // refDTypep is retargeted for captured refs by resolveCapturedRefs, and no + // non-captured ref resolves refDTypep into a template that then dies, so it + // never survives pointing at a dead module here (verifyNoDeadRefs re-checks). + AstNodeDType* const oldRefDTypep = refp->refDTypep(); + if (oldRefDTypep && liveNodes.count(oldRefDTypep)) { + AstNodeModule* const targetModp = findOwnerModuleIfLive(oldRefDTypep, liveNodes); + UASSERT_OBJ(!targetModp || !targetModp->dead(), refp, + "refDTypep of '" << refp->prettyNameQ() << "' points to dead module '" + << (targetModp ? targetModp->name() : "") << "'"); } - // Fix base-class dtypep() - V3Broken checks this pointer, and V3Width - // may have set it to a node in the dead template module. Derive from - // the (already fixed) typedefp chain when possible. - if (refp->dtypep()) { - AstNodeModule* const dtOwnerp = findOwnerModule(refp->dtypep()); - if (dtOwnerp && dtOwnerp->dead()) { - AstNodeDType* newDtp = nullptr; - // Derive from the fixed typedef's subDTypep. This always succeeds - // because the typedefp was fixed above to point to a clone's typedef - // whose subDTypep is a live type-table entry or clone-owned dtype. - // If this fires, either typedefp was not fixed or subDTypep is stale. - // Dump refp->typedefp() and dtOwnerp to diagnose. - if (refp->typedefp() && refp->typedefp()->subDTypep()) { - newDtp = refp->typedefp()->subDTypep(); - AstNodeModule* const newDtOwnerp = findOwnerModule(newDtp); - if (newDtOwnerp && newDtOwnerp->dead()) newDtp = nullptr; - } - UASSERT_OBJ(newDtp, refp, - "fixDeadRefs dtypep: could not derive live dtypep for " - << refp->prettyNameQ() << " dead owner=" << dtOwnerp->name() - << " typedefp=" - << (refp->typedefp() ? refp->typedefp()->name() : "")); - UINFO(9, "iface capture finalizeCapture (" - << location << "): fixing dtypep refp=" << refp - << " dead=" << dtOwnerp->name() << " -> " << newDtp); - refp->dtypep(newDtp); - ++fixed; - } + // dtypep (checked later by V3Broken) likewise never points at a dead module. + AstNodeDType* const oldDTypep = refp->dtypep(); + if (oldDTypep && liveNodes.count(oldDTypep)) { + AstNodeModule* const dtOwnerp = findOwnerModuleIfLive(oldDTypep, liveNodes); + UASSERT_OBJ(!dtOwnerp || !dtOwnerp->dead(), refp, + "dtypep of '" << refp->prettyNameQ() << "' points to dead module '" + << (dtOwnerp ? dtOwnerp->name() : "") << "'"); } return fixed; @@ -332,26 +346,23 @@ AstNodeModule* V3LinkDotIfaceCapture::findLiveCloneOf(AstNodeModule* deadTargetM } AstNodeModule* V3LinkDotIfaceCapture::findOwnerModule(AstNode* nodep) { - for (AstNode* curp = nodep; curp; curp = curp->backp()) { - // Guard against corrupted backp() chains (e.g. freed memory, - // low addresses like 0x1) from nodes unlinked by linkDotParamed. - if (reinterpret_cast(curp) < 0x1000) return nullptr; - if (AstNodeModule* const modp = VN_CAST(curp, NodeModule)) return modp; + return findOwnerModuleImpl(nodep, nullptr); +} + +void V3LinkDotIfaceCapture::nullStaleLedgerRefs(const std::unordered_set& live) { + for (auto& kv : s_map) { + kv.second.foreachLink([&](AstNode*& nodep) { + if (nodep && !live.count(nodep)) nodep = nullptr; + }); } - return nullptr; } void V3LinkDotIfaceCapture::purgeStaleRefs() { if (!s_enabled || s_map.empty() || !v3Global.rootp()) return; // Collect every live AstNode* in the AST so we can detect stale pointers // in the ledger (refp, ownerModp, typedefp, paramTypep, etc.). - std::unordered_set liveNodes; - v3Global.rootp()->foreach([&](AstNode* np) { liveNodes.insert(np); }); - for (auto& kv : s_map) { - kv.second.foreachLink([&](AstNode*& nodep) { - if (nodep && !liveNodes.count(nodep)) nodep = nullptr; - }); - } + const LiveNodes liveNodes = collectLiveNodes(); + nullStaleLedgerRefs(liveNodes); } void V3LinkDotIfaceCapture::dumpEntries(const string& label) { @@ -404,10 +415,18 @@ void V3LinkDotIfaceCapture::add(AstRefDType* refp, const string& cellPath, << refp->name() << " cellPath='" << cellPath << "'" << " ownerMod=" << ownerModName << " extraRefps.size=" << it->second.extraRefps.size()); } else { - s_map[key] = CapturedEntry{ - CaptureType::IFACE, refp, cellPath, - /*cloneCellPath=*/"", - /*origClassp=*/nullptr, ownerModp, typedefp, nullptr, tdOwnerName, ifacePortVarp, {}}; + s_map[key] = CapturedEntry{CaptureType::IFACE, + TargetKind::TYPEDEF, + refp, + cellPath, + /*cloneCellPath=*/"", + /*origClassp=*/nullptr, + ownerModp, + typedefp, + nullptr, + tdOwnerName, + ifacePortVarp, + {}}; UINFO(9, "iface capture add: refp=" << refp->name() << " cellPath='" << cellPath << "'" << " ownerMod=" << ownerModName << " typedefp=" << (typedefp ? typedefp->name() : "") @@ -428,9 +447,18 @@ void V3LinkDotIfaceCapture::addClass(AstRefDType* refp, AstClass* origClassp, UASSERT_OBJ(!cellPath.empty(), origClassp, "addClass() produced empty cellPath"); const string ownerModName = ownerModp->name(); const CaptureKey key{ownerModName, refp->name(), cellPath, ""}; - s_map[key] = CapturedEntry{CaptureType::CLASS, refp, cellPath, - /*cloneCellPath=*/"", origClassp, ownerModp, typedefp, nullptr, - tdOwnerName, nullptr, {}}; + s_map[key] = CapturedEntry{CaptureType::CLASS, + TargetKind::TYPEDEF, + refp, + cellPath, + /*cloneCellPath=*/"", + origClassp, + ownerModp, + typedefp, + nullptr, + tdOwnerName, + nullptr, + {}}; UINFO(9, "iface capture addClass: refp=" << refp->name() << " cellPath='" << cellPath << "'" << " ownerMod=" << (ownerModp ? ownerModp->name() : "")); @@ -518,8 +546,10 @@ AstNodeModule* V3LinkDotIfaceCapture::followCellPath(AstNodeModule* startModp, // after all cell pointers are wired to the correct interface clones. // See header ARCHITECTURE comment for the full picture. void V3LinkDotIfaceCapture::propagateClone(const TemplateKey& tkey, AstRefDType* newRefp, + AstNodeModule* newOwnerModp, const string& cloneCellPath) { UASSERT(newRefp, "propagateClone() called with null newRefp"); + UASSERT(newOwnerModp, "propagateClone() called with null newOwnerModp"); // Find the template entry by exact key. The entry was captured during // the primary LinkDot pass, so it must exist. If this fires, either the // capture was missed or the key components (ownerModName, refName, @@ -536,6 +566,7 @@ void V3LinkDotIfaceCapture::propagateClone(const TemplateKey& tkey, AstRefDType* // where cell pointers are already wired to the correct interface clones. CapturedEntry newEntry = it->second; newEntry.refp = newRefp; + newEntry.ownerModp = newOwnerModp; newEntry.cellPath = tkey.cellPath; newEntry.cloneCellPath = cloneCellPath; newEntry.clearStaleRefs(); @@ -696,6 +727,7 @@ void V3LinkDotIfaceCapture::captureInnerParamTypeRefs(AstParamTypeDType* paramTy << innerRefp << " refDTypep owner=" << refOwnerModp->name() << " nestedCellName='" << nestedCellName << "'"); s_map[innerKey] = CapturedEntry{CaptureType::IFACE, + TargetKind::TYPEDEF, innerRefp, nestedCellName.empty() ? cellPath : nestedCellName, /*cloneCellPath=*/"", @@ -746,11 +778,18 @@ void V3LinkDotIfaceCapture::addParamType(AstRefDType* refp, const string& cellPa << refp->name() << " cellPath='" << cellPath << "'" << " ownerMod=" << ownerModName << " extraRefps.size=" << it->second.extraRefps.size()); } else { - s_map[key] - = CapturedEntry{CaptureType::IFACE, refp, cellPath, - /*cloneCellPath=*/"", - /*origClassp=*/nullptr, ownerModp, nullptr, paramTypep, ptOwnerName, - ifacePortVarp, {}}; + s_map[key] = CapturedEntry{CaptureType::IFACE, + TargetKind::PARAM_TYPE, + refp, + cellPath, + /*cloneCellPath=*/"", + /*origClassp=*/nullptr, + ownerModp, + nullptr, + paramTypep, + ptOwnerName, + ifacePortVarp, + {}}; } // Also capture REFDTYPEs inside the PARAMTYPEDTYPE's subDTypep chain. @@ -766,6 +805,7 @@ void V3LinkDotIfaceCapture::addParamType(AstRefDType* refp, const string& cellPa // Handles both AstRefDType (direct typedef references) and AstMemberDType // (struct/union member types) in a single traversal for efficiency. class TypeTableDeadRefVisitor final : public VNVisitor { + const LiveNodes& m_liveNodes; int m_fixed = 0; void visit(AstRefDType* refp) override { @@ -776,26 +816,27 @@ class TypeTableDeadRefVisitor final : public VNVisitor { AstNodeModule* deadTargetModp = nullptr; // Check BOTH typedefp and refDTypep for dead owners. // Either (or both) may point to a dead module. - if (refp->typedefp()) { - AstNodeModule* const tdOwnerp - = V3LinkDotIfaceCapture::findOwnerModule(refp->typedefp()); + if (refp->typedefp() && m_liveNodes.count(refp->typedefp())) { + AstNodeModule* const tdOwnerp = findOwnerModuleIfLive(refp->typedefp(), m_liveNodes); if (tdOwnerp && tdOwnerp->dead()) deadTargetModp = tdOwnerp; } - if (!deadTargetModp && refp->refDTypep()) { - AstNodeModule* const rdOwnerp - = V3LinkDotIfaceCapture::findOwnerModule(refp->refDTypep()); + if (!deadTargetModp && refp->refDTypep() && m_liveNodes.count(refp->refDTypep())) { + AstNodeModule* const rdOwnerp = findOwnerModuleIfLive(refp->refDTypep(), m_liveNodes); if (rdOwnerp && rdOwnerp->dead()) deadTargetModp = rdOwnerp; } if (deadTargetModp) { V3LinkDotIfaceCapture::findLiveCloneOf(deadTargetModp, &containingModp); } - m_fixed += V3LinkDotIfaceCapture::fixDeadRefs(refp, containingModp, "type table"); + m_fixed + += V3LinkDotIfaceCapture::fixDeadRefs(refp, containingModp, "type table", m_liveNodes); } void visit(AstMemberDType* memberp) override { iterateChildren(memberp); if (!memberp->dtypep()) return; - AstNodeModule* const dtOwnerp = V3LinkDotIfaceCapture::findOwnerModule(memberp->dtypep()); + UASSERT_OBJ(m_liveNodes.count(memberp->dtypep()), memberp, + "MemberDType has a dangling dtypep"); + AstNodeModule* const dtOwnerp = findOwnerModuleIfLive(memberp->dtypep(), m_liveNodes); if (!dtOwnerp || !dtOwnerp->dead()) return; // Try to find the clone of the dead module AstNodeModule* const cloneModp = V3LinkDotIfaceCapture::findLiveCloneOf(dtOwnerp); @@ -830,29 +871,33 @@ class TypeTableDeadRefVisitor final : public VNVisitor { public: int fixed() const { return m_fixed; } - explicit TypeTableDeadRefVisitor(AstNode* nodep) { iterate(nodep); } + TypeTableDeadRefVisitor(AstNode* nodep, const LiveNodes& liveNodes) + : m_liveNodes{liveNodes} { + iterate(nodep); + } }; -int V3LinkDotIfaceCapture::fixDeadRefsInTypeTable() { +int V3LinkDotIfaceCapture::fixDeadRefsInTypeTable(const LiveNodes& liveNodes) { if (!v3Global.rootp()->typeTablep()) return 0; - const TypeTableDeadRefVisitor visitor{v3Global.rootp()->typeTablep()}; + const TypeTableDeadRefVisitor visitor{v3Global.rootp()->typeTablep(), liveNodes}; return visitor.fixed(); } -int V3LinkDotIfaceCapture::fixDeadRefsInModules() { +int V3LinkDotIfaceCapture::fixDeadRefsInModules(const LiveNodes& liveNodes) { int fixed = 0; for (AstNode* nodep = v3Global.rootp()->modulesp(); nodep; nodep = nodep->nextp()) { if (AstNodeModule* const modp = VN_CAST(nodep, NodeModule)) { if (modp->dead()) continue; const string modName = modp->name(); - modp->foreach( - [&](AstRefDType* refp) { fixed += fixDeadRefs(refp, modp, modName.c_str()); }); + modp->foreach([&](AstRefDType* refp) { + fixed += fixDeadRefs(refp, modp, modName.c_str(), liveNodes); + }); } } return fixed; } -int V3LinkDotIfaceCapture::fixWrongCloneRefs() { +int V3LinkDotIfaceCapture::resolveCapturedRefs() { int fixed = 0; // TARGET RESOLUTION - the ONLY place that resolves targets and @@ -865,78 +910,58 @@ int V3LinkDotIfaceCapture::fixWrongCloneRefs() { forEach([&](const CapturedEntry& entry) { AstRefDType* const refp = entry.refp; if (!refp) return; - // For clone entries the stored ownerModp is the template (stale cells). - // Use the actual module containing the REFDTYPE - its cells are wired - // to the correct interface clones by this point. - // findOwnerModule handles corrupted backp() chains gracefully. - AstNodeModule* const ownerModp - = !entry.cloneCellPath.empty() ? findOwnerModule(refp) : entry.ownerModp; + // Parameterized class typedefs are relinked by + // ParamClassRefDTypeRelinkVisitor. Their class name is not a cell path. + if (entry.captureType == CaptureType::CLASS) return; + AstNodeModule* const ownerModp = entry.ownerModp; if (!ownerModp || ownerModp->dead() || VN_IS(ownerModp, Package)) return; - AstNodeModule* const rdOwnerBefore - = (refp->refDTypep() ? findOwnerModule(refp->refDTypep()) : nullptr); UINFO(9, "finalizeIfaceCapture Phase3 entry: refp=" << refp->name() << " (" << cvtToHex(refp) << ")" << " ownerMod=" << ownerModp->name() << " (dead=" << ownerModp->dead() << ")" << " storedOwnerMod=" << (entry.ownerModp ? entry.ownerModp->name() : "") << " cellPath='" << entry.cellPath << "' cloneCellPath='" << entry.cloneCellPath - << "' typedefp=" << (refp->typedefp() ? refp->typedefp()->name() : "") - << " refDTypep=" << (refp->refDTypep() ? refp->refDTypep()->name() : "") - << " refDTypepOwner=" << (rdOwnerBefore ? rdOwnerBefore->name() : "") - << " refDTypepDead=" << (rdOwnerBefore ? rdOwnerBefore->dead() : 0)); + << "' targetKind=" + << (entry.targetKind == TargetKind::PARAM_TYPE ? "param type" : "typedef")); - // Determine the correct target module using cellPath + // Prefer the owner itself when its stable template identity matches the + // captured target owner. Otherwise resolve and validate the cell path. AstNodeModule* correctModp = nullptr; - if (!entry.cellPath.empty()) { + if (moduleMatchesOwner(ownerModp, entry.typedefOwnerModName)) { + correctModp = ownerModp; + } else { + // A non-matching owner always carries a cell path to the target owner. + UASSERT_OBJ(!entry.cellPath.empty(), refp, + "captured ref '" + << refp->prettyNameQ() << "' owner '" << ownerModp->prettyNameQ() + << "' does not match target owner '" << entry.typedefOwnerModName + << "' and has no cell path"); correctModp = followCellPath(ownerModp, entry.cellPath); UINFO(9, " followCellPath('" << ownerModp->name() << "', '" << entry.cellPath << "') = " << (correctModp ? correctModp->name() : "") << (correctModp ? (correctModp->dead() ? " (DEAD)" : " (live)") : "")); - if (correctModp && correctModp->dead()) { correctModp = nullptr; } + UASSERT_OBJ(correctModp && !correctModp->dead() + && moduleMatchesOwner(correctModp, entry.typedefOwnerModName), + refp, + "captured ref '" << refp->prettyNameQ() << "' cell path '" + << entry.cellPath << "' did not resolve to live owner '" + << entry.typedefOwnerModName << "'"); } - - // Proactive target resolution: when cellPath resolved to a valid - // correctModp, find the PARAMTYPEDTYPE or TYPEDEF by name and apply. - if (correctModp) { - const string& refName = refp->name(); - bool resolved = false; - if (AstParamTypeDType* const ptdp = findParamTypeInModule(correctModp, refName)) { - refp->refDTypep(ptdp); - refp->user3(true); - resolved = true; - UINFO(9, "finalizeIfaceCapture Phase3: resolved paramTypep '" - << refName << "' in " << correctModp->name() << " for refp in " - << ownerModp->name() << " cloneCellPath='" << entry.cloneCellPath - << "'"); - } else if (AstTypedef* const tdp = findTypedefInModule(correctModp, refName)) { - refp->typedefp(tdp); - refp->user3(true); - resolved = true; - UINFO(9, "finalizeIfaceCapture Phase3: resolved typedefp '" - << refName << "' in " << correctModp->name() << " for refp in " - << ownerModp->name() << " cloneCellPath='" << entry.cloneCellPath - << "'"); - } - if (resolved) { - ++fixed; - return; - } - } - - // Note: the structural disambiguation infrastructure (collectReachable, - // findCorrectClone, wrong-clone fixup blocks) was removed. All captured - // entries have non-empty cellPath, and disambiguateTarget always returns - // nullptr through the "cellPath unresolved" path. The wrong-clone fixup - // blocks were dead code - CI-CD with v3fatalSrc asserts confirmed this. - // Unresolved entries are fixed by fixDeadRefs in a later phase. + UASSERT_OBJ( + retargetRefToModule(entry, correctModp), refp, + "could not retarget captured " + << (entry.targetKind == TargetKind::PARAM_TYPE ? "parameter type " : "typedef ") + << refp->prettyNameQ() << " in " << correctModp->prettyNameQ()); + refp->user3(true); + ++fixed; }); return fixed; } -void V3LinkDotIfaceCapture::verifyNoDeadRefs() { +void V3LinkDotIfaceCapture::verifyNoDeadRefs(const LiveNodes& liveNodes) { // Assert: no REFDTYPE in any live module should have typedefp or refDTypep // pointing to a dead module. for (AstNode* nodep = v3Global.rootp()->modulesp(); nodep; nodep = nodep->nextp()) { @@ -944,40 +969,26 @@ void V3LinkDotIfaceCapture::verifyNoDeadRefs() { if (modp->dead()) continue; modp->foreach([&](AstRefDType* refp) { if (refp->typedefp()) { - AstNodeModule* const ownerModp = findOwnerModule(refp->typedefp()); - // Diagnostic block: only entered when fixup logic has a bug - // and leaves a typedefp pointing to a dead module. - if (ownerModp && ownerModp->dead()) { // LCOV_EXCL_START - bool inLedger = false; - forEach([&](const CapturedEntry& e) { - if (e.refp == refp) inLedger = true; - }); - UINFO(9, "VERIFY FAIL typedefp: refp=" - << refp->name() << " (" << cvtToHex(refp) << ")" << " in mod=" - << modp->name() << " typedefp->owner=" << ownerModp->name() - << " inLedger=" << inLedger); - } // LCOV_EXCL_STOP - UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp, // LCOV_EXCL_LINE + UASSERT_OBJ(liveNodes.count(refp->typedefp()), refp, + "REFDTYPE '" << refp->prettyNameQ() << "' in live module '" + << modp->prettyNameQ() + << "' has a dangling typedefp"); + AstNodeModule* const ownerModp + = findOwnerModuleIfLive(refp->typedefp(), liveNodes); + UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp, "REFDTYPE '" << refp->prettyNameQ() << "' in live module '" << modp->prettyNameQ() << "' has typedefp pointing to dead module '" << ownerModp->prettyNameQ() << "'"); } if (refp->refDTypep()) { - AstNodeModule* const ownerModp = findOwnerModule(refp->refDTypep()); - // Diagnostic block: only entered when fixup logic has a bug - // and leaves a refDTypep pointing to a dead module. - if (ownerModp && ownerModp->dead()) { // LCOV_EXCL_START - bool inLedger = false; - forEach([&](const CapturedEntry& e) { - if (e.refp == refp) inLedger = true; - }); - UINFO(9, "VERIFY FAIL refDTypep: refp=" - << refp->name() << " (" << cvtToHex(refp) << ")" << " in mod=" - << modp->name() << " refDTypep->owner=" << ownerModp->name() - << " inLedger=" << inLedger); - } // LCOV_EXCL_STOP - UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp, // LCOV_EXCL_LINE + UASSERT_OBJ(liveNodes.count(refp->refDTypep()), refp, + "REFDTYPE '" << refp->prettyNameQ() << "' in live module '" + << modp->prettyNameQ() + << "' has a dangling refDTypep"); + AstNodeModule* const ownerModp + = findOwnerModuleIfLive(refp->refDTypep(), liveNodes); + UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp, "REFDTYPE '" << refp->prettyNameQ() << "' in live module '" << modp->prettyNameQ() << "' has refDTypep pointing to dead module '" @@ -991,20 +1002,24 @@ void V3LinkDotIfaceCapture::verifyNoDeadRefs() { nodep = nodep->nextp()) { nodep->foreach([&](AstRefDType* refp) { if (refp->typedefp()) { - AstNodeModule* const ownerModp = findOwnerModule(refp->typedefp()); - // Bug-only assertion: fires only if fixup logic fails to - // resolve a type-table typedefp away from a dead module. - UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp, // LCOV_EXCL_LINE + UASSERT_OBJ(liveNodes.count(refp->typedefp()), refp, + "REFDTYPE '" << refp->prettyNameQ() + << "' in type table has a dangling typedefp"); + AstNodeModule* const ownerModp + = findOwnerModuleIfLive(refp->typedefp(), liveNodes); + UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp, "REFDTYPE '" << refp->prettyNameQ() << "' in type table has typedefp pointing to dead module '" << ownerModp->prettyNameQ() << "'"); } if (refp->refDTypep()) { - AstNodeModule* const ownerModp = findOwnerModule(refp->refDTypep()); - // Bug-only assertion: fires only if fixup logic fails to - // resolve a type-table refDTypep away from a dead module. - UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp, // LCOV_EXCL_LINE + UASSERT_OBJ(liveNodes.count(refp->refDTypep()), refp, + "REFDTYPE '" << refp->prettyNameQ() + << "' in type table has a dangling refDTypep"); + AstNodeModule* const ownerModp + = findOwnerModuleIfLive(refp->refDTypep(), liveNodes); + UASSERT_OBJ(!ownerModp || !ownerModp->dead(), refp, "REFDTYPE '" << refp->prettyNameQ() << "' in type table has refDTypep pointing to dead module '" @@ -1021,14 +1036,23 @@ void V3LinkDotIfaceCapture::finalizeIfaceCapture() { if (!v3Global.rootp()) return; clearModuleCache(); // Ensure fresh view after all cloning/widthing - const int typeTableFixed = fixDeadRefsInTypeTable(); - const int moduleFixed = fixDeadRefsInModules(); + // purgeStaleRefs() snapshotted liveness at the end of V3Param::param() and + // discarded it; linkDotParamed, finalizeDeferredParams, linkLValue and linkWith + // have since mutated the tree. Take a fresh snapshot and reuse it here both to + // purge the ledger and to guard every legacy cross-link inspection. + const LiveNodes liveNodes = collectLiveNodes(); + nullStaleLedgerRefs(liveNodes); + + // Resolve live captured refs from stable path metadata before inspecting + // any inherited target pointers, which may refer to replaced template nodes. + const int capturedFixed = resolveCapturedRefs(); + UINFO(4, "finalizeIfaceCapture: structurally resolved " << capturedFixed << " captured refs"); + + const int typeTableFixed = fixDeadRefsInTypeTable(liveNodes); + const int moduleFixed = fixDeadRefsInModules(liveNodes); UINFO(4, "finalizeIfaceCapture: fixed " << typeTableFixed << " in type table, " << moduleFixed << " in modules (dead refs)"); - const int wrongCloneFixed = fixWrongCloneRefs(); - UINFO(4, "finalizeIfaceCapture: fixed " << wrongCloneFixed << " wrong-live-clone pointers"); - if (debug() >= 9) dumpEntries("after finalizeIfaceCapture"); // Emit statistics for --stats @@ -1046,8 +1070,10 @@ void V3LinkDotIfaceCapture::finalizeIfaceCapture() { V3Stats::addStat("IfaceCapture, Entries cloned", clones); V3Stats::addStat("IfaceCapture, Dead refs fixed in type table", typeTableFixed); V3Stats::addStat("IfaceCapture, Dead refs fixed in modules", moduleFixed); - V3Stats::addStat("IfaceCapture, Wrong-clone refs fixed", wrongCloneFixed); + V3Stats::addStat("IfaceCapture, Captured refs resolved", capturedFixed); - verifyNoDeadRefs(); + // Independent debug-only audit of the repairs above; kept separate from the + // repair traversal so it can catch omissions in that repair. + if (debug() >= 9) verifyNoDeadRefs(liveNodes); reset(); } diff --git a/src/V3LinkDotIfaceCapture.h b/src/V3LinkDotIfaceCapture.h index b3a8dbc4c..bce20aff8 100644 --- a/src/V3LinkDotIfaceCapture.h +++ b/src/V3LinkDotIfaceCapture.h @@ -28,6 +28,7 @@ #include #include #include +#include #include class VSymEnt; @@ -35,6 +36,7 @@ class VSymEnt; class V3LinkDotIfaceCapture final { public: enum class CaptureType : uint8_t { IFACE, CLASS }; + enum class TargetKind : uint8_t { TYPEDEF, PARAM_TYPE }; // Path-based map key: no pointers, only stable strings. // {ownerModName, refName, cellPath, cloneCellPath} uniquely identifies @@ -70,6 +72,8 @@ public: struct CapturedEntry final { CaptureType captureType = CaptureType::IFACE; + // Semantic target identity, retained when template pointers are cleared. + TargetKind targetKind = TargetKind::TYPEDEF; AstRefDType* refp = nullptr; string cellPath; // Template path (e.g. "cca_io.tlb_io") - immutable key component string cloneCellPath; // Instance-specific path (e.g. "cca_io1.tlb_io") - set by @@ -131,19 +135,20 @@ private: static void clearModuleCache(); static AstIfaceRefDType* ifaceRefFromVarDType(AstNodeDType* dtypep); static string extractIfacePortName(const string& dotText); - static AstNodeDType* findDTypeByPrettyName(AstNodeModule* modp, const string& prettyName); static AstNodeModule* findCloneViaHierarchy(AstNodeModule* containingModp, AstNodeModule* deadTargetModp, int depth = 0); static AstNodeModule* findLiveCloneOf(AstNodeModule* deadTargetModp, AstNodeModule** containerp = nullptr); - static int fixDeadRefs(AstRefDType* refp, AstNodeModule* containingModp, const char* location); + static int fixDeadRefs(AstRefDType* refp, AstNodeModule* containingModp, const char* location, + const std::unordered_set& liveNodes); static void captureInnerParamTypeRefs(AstParamTypeDType* paramTypep, AstRefDType* refp, const string& cellPath, const string& ownerModName, const string& ptOwnerName); - static int fixDeadRefsInTypeTable(); - static int fixDeadRefsInModules(); - static int fixWrongCloneRefs(); - static void verifyNoDeadRefs(); + static void nullStaleLedgerRefs(const std::unordered_set& liveNodes); + static int fixDeadRefsInTypeTable(const std::unordered_set& liveNodes); + static int fixDeadRefsInModules(const std::unordered_set& liveNodes); + static int resolveCapturedRefs(); + static void verifyNoDeadRefs(const std::unordered_set& liveNodes); template static void forEachImpl(T_FilterFn&& filter, T_Fn&& fn); @@ -156,6 +161,8 @@ public: static AstNodeDType* findDTypeInModule(AstNodeModule* modp, const string& name, VNType type); // Find a ParamTypeDType by name in a module's top-level statements static AstParamTypeDType* findParamTypeInModule(AstNodeModule* modp, const string& name); + // Retarget every live RefDType in an entry using only stable capture metadata. + static bool retargetRefToModule(const CapturedEntry& entry, AstNodeModule* targetModp); static void add(AstRefDType* refp, const string& cellPath, AstNodeModule* ownerModp, AstTypedef* typedefp = nullptr, const string& typedefOwnerModName = "", AstVar* ifacePortVarp = nullptr); @@ -182,7 +189,7 @@ public: // Ledger-only: no target lookup or AST mutation. Target resolution // happens later in finalizeIfaceCapture where cell pointers are wired up. static void propagateClone(const TemplateKey& tkey, AstRefDType* newRefp, - const string& cloneCellPath); + AstNodeModule* newOwnerModp, const string& cloneCellPath); static void captureTypedefContext(AstRefDType* refp, const char* stageLabel, int dotPos, bool dotIsFinal, const std::string& dotText, @@ -190,8 +197,8 @@ public: AstNode* nodep, const std::function& indentFn); - // Null out ledger refp entries that point to freed nodes (not in the live AST). - // Called once after V3Param completes, before any code touches the ledger. + // Null out ledger entries that point to freed nodes (not in the live AST). + // Called at pass boundaries before code dereferences ledger pointers. static void purgeStaleRefs(); // Debug: dump all captured entries diff --git a/src/V3Param.cpp b/src/V3Param.cpp index 251d8909d..3e4e960d5 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -850,54 +850,6 @@ class ParamProcessor final { 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; - } - } else if (!entry.cloneCellPath.empty()) { - // Clone entry has no paramTypep stored; look up the type by name. - if (AstParamTypeDType* const ptp - = V3LinkDotIfaceCapture::findParamTypeInModule(targetModp, entry.refp->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. @@ -915,7 +867,10 @@ class ParamProcessor final { V3LinkDotIfaceCapture::forEach([&](const V3LinkDotIfaceCapture::CapturedEntry& entry) { if (!entry.refp) return; if (entry.cloneCellPath != cloneCP) return; - if (!entry.ownerModp || entry.ownerModp->name() != srcName) return; + UASSERT_OBJ( + entry.ownerModp + && (entry.ownerModp == newModp || entry.ownerModp->name() == srcName), + entry.refp, "clone ledger entry for '" << cloneCP << "' has unexpected owner"); if (entry.cellPath.empty()) return; AstRefDType* const refp = entry.refp; @@ -1048,17 +1003,24 @@ class ParamProcessor final { if (AstRefDType* const clonedRefp = entry.refp->clonep()) { // Use newname (unique specialized module name) as cloneCellPath. const string cloneCP = newname; + // A cloned captured ref lives inside srcModp's tree, so its owner + // is srcModp (SV has no nested module definitions). + UASSERT_OBJ( + entry.ownerModp == srcModp, clonedRefp, + "cloned captured RefDType owner is not the specialized module"); + AstNodeModule* const clonedOwnerp = newModp; const V3LinkDotIfaceCapture::TemplateKey tkey{ entry.ownerModp ? entry.ownerModp->name() : "", entry.refp->name(), entry.cellPath}; - V3LinkDotIfaceCapture::propagateClone(tkey, clonedRefp, cloneCP); + V3LinkDotIfaceCapture::propagateClone(tkey, clonedRefp, clonedOwnerp, + 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)) { + if (V3LinkDotIfaceCapture::retargetRefToModule(entry, newModp)) { UINFO(9, "iface capture direct retarget: " << entry.refp << " -> " << newModp->prettyNameQ()); } @@ -1078,7 +1040,7 @@ class ParamProcessor final { && !cellPathMatchesClone(entry.cellPath, cloneCellp, actualOwnerp, m_modp)) { return; } - if (retargetRefToModule(entry, newModp)) { + if (V3LinkDotIfaceCapture::retargetRefToModule(entry, newModp)) { UINFO(9, "iface capture clone-entry retarget: " << entry.refp << " -> " << newModp->prettyNameQ()); } @@ -2185,7 +2147,7 @@ public: && !(ownerp == nullptr && entry.cloneCellPath == parentModp->name())) { return; } - if (retargetRefToModule(entry, correctModp)) { + if (V3LinkDotIfaceCapture::retargetRefToModule(entry, correctModp)) { UINFO(9, "retargetIfaceRefs: " << entry.refp << " -> " << correctModp->prettyNameQ()); } diff --git a/test_regress/t/t_iface_typedef_capture_uaf.py b/test_regress/t/t_iface_typedef_capture_uaf.py new file mode 100755 index 000000000..44d191244 --- /dev/null +++ b/test_regress/t/t_iface_typedef_capture_uaf.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# 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: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.compile(v_flags2=["--binary"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_iface_typedef_capture_uaf.v b/test_regress/t/t_iface_typedef_capture_uaf.v new file mode 100644 index 000000000..d36019e5a --- /dev/null +++ b/test_regress/t/t_iface_typedef_capture_uaf.v @@ -0,0 +1,55 @@ +// DESCRIPTION: Verilator: Test interface typedef capture after parameter cloning +// +// 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: 2026 Wilson Snyder +// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +// verilator lint_off DECLFILENAME + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d (%s !== %s)\n", `__FILE__,`__LINE__, (gotv), (expv), `"gotv`", `"expv`"); `stop; end while(0) +// verilog_format: on + +interface avmm_if #( + parameter int DW = 32 +); + typedef struct packed { + logic [DW-1:0] writedata; + logic write; + } request_t; + + request_t req; + + modport master(output req); + modport slave(input req); +endinterface + +module avmm_autopipeline ( + avmm_if.slave master, + avmm_if.master slave +); + typedef master.request_t request_t; + request_t internal_req; + + assign internal_req = master.req; + always_comb slave.req = internal_req; +endmodule + +module t; + for (genvar g = 0; g < 2; ++g) begin : gen_block + avmm_if #(.DW(32 * (g + 1))) m_if (); + avmm_if #(.DW(32 * (g + 1))) s_if (); + avmm_autopipeline pipe (.master(m_if), .slave(s_if)); + end + + initial begin + #1; + `checkd($bits(gen_block[0].pipe.internal_req), 33); + `checkd($bits(gen_block[1].pipe.internal_req), 65); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_paramgraph_comined_iface_stats.py b/test_regress/t/t_paramgraph_comined_iface_stats.py index 4732b2b49..a236a6003 100755 --- a/test_regress/t/t_paramgraph_comined_iface_stats.py +++ b/test_regress/t/t_paramgraph_comined_iface_stats.py @@ -22,7 +22,7 @@ test.file_grep(test.stats, r'IfaceCapture, Entries total\s+(\d+)', 18) test.file_grep(test.stats, r'IfaceCapture, Entries template\s+(\d+)', 8) test.file_grep(test.stats, r'IfaceCapture, Entries cloned\s+(\d+)', 10) test.file_grep(test.stats, r'IfaceCapture, Ledger fixups in V3Param\s+(\d+)', 8) -test.file_grep(test.stats, r'IfaceCapture, Wrong-clone refs fixed\s+(\d+)', 10) +test.file_grep(test.stats, r'IfaceCapture, Captured refs resolved\s+(\d+)', 10) test.file_grep(test.stats, r'IfaceCapture, Dead refs fixed in modules\s+(\d+)', 0) test.execute() diff --git a/test_regress/t/t_paramgraph_iface_template_nested_stats.py b/test_regress/t/t_paramgraph_iface_template_nested_stats.py index f6e57844b..cab06e1a1 100755 --- a/test_regress/t/t_paramgraph_iface_template_nested_stats.py +++ b/test_regress/t/t_paramgraph_iface_template_nested_stats.py @@ -22,7 +22,7 @@ test.file_grep(test.stats, r'IfaceCapture, Entries total\s+(\d+)', 25) test.file_grep(test.stats, r'IfaceCapture, Entries template\s+(\d+)', 11) test.file_grep(test.stats, r'IfaceCapture, Entries cloned\s+(\d+)', 14) test.file_grep(test.stats, r'IfaceCapture, Ledger fixups in V3Param\s+(\d+)', 5) -test.file_grep(test.stats, r'IfaceCapture, Wrong-clone refs fixed\s+(\d+)', 10) +test.file_grep(test.stats, r'IfaceCapture, Captured refs resolved\s+(\d+)', 16) test.file_grep(test.stats, r'IfaceCapture, Dead refs fixed in modules\s+(\d+)', 0) test.execute() diff --git a/test_regress/t/t_paramgraph_nested_iface_typedef_stats.py b/test_regress/t/t_paramgraph_nested_iface_typedef_stats.py index e0142486f..9f9ad34f9 100755 --- a/test_regress/t/t_paramgraph_nested_iface_typedef_stats.py +++ b/test_regress/t/t_paramgraph_nested_iface_typedef_stats.py @@ -22,7 +22,7 @@ test.file_grep(test.stats, r'IfaceCapture, Entries total\s+(\d+)', 20) test.file_grep(test.stats, r'IfaceCapture, Entries template\s+(\d+)', 8) test.file_grep(test.stats, r'IfaceCapture, Entries cloned\s+(\d+)', 12) test.file_grep(test.stats, r'IfaceCapture, Ledger fixups in V3Param\s+(\d+)', 8) -test.file_grep(test.stats, r'IfaceCapture, Wrong-clone refs fixed\s+(\d+)', 14) +test.file_grep(test.stats, r'IfaceCapture, Captured refs resolved\s+(\d+)', 14) test.file_grep(test.stats, r'IfaceCapture, Dead refs fixed in modules\s+(\d+)', 0) test.execute()