diff --git a/src/V3Inline.cpp b/src/V3Inline.cpp index 46e76921b..51c721e30 100644 --- a/src/V3Inline.cpp +++ b/src/V3Inline.cpp @@ -158,8 +158,8 @@ class InlineMarkVisitor final : public VNVisitor { } } void visit(AstVarXRef* nodep) override { - // Remove link. V3LinkDot will reestablish it after inlining. - nodep->varp(nullptr); + // Keep varp - V3Const::constifyEdit is called during pinReconnectSimple + // which needs varp to be set. V3LinkDot will re-resolve after inlining. } void visit(AstNodeFTaskRef* nodep) override { // Remove link. V3LinkDot will reestablish it after inlining. @@ -322,7 +322,7 @@ class InlineRelinkVisitor final : public VNVisitor { } void visit(AstVarRef* nodep) override { // If the target port is being inlined, replace reference with the - // connected expression (always a Const of a VarRef). + // connected expression (a Const, VarRef, or VarXRef). AstNode* const pinExpr = nodep->varp()->user2p(); if (!pinExpr) return; @@ -354,10 +354,25 @@ class InlineRelinkVisitor final : public VNVisitor { return; } - // Otherwise it must be a variable reference, retarget this ref - const AstVarRef* const vrefp = VN_AS(pinExpr, VarRef); - nodep->varp(vrefp->varp()); - nodep->classOrPackagep(vrefp->classOrPackagep()); + // Handle VarRef: simple retarget + if (const AstVarRef* const vrefp = VN_CAST(pinExpr, VarRef)) { + nodep->varp(vrefp->varp()); + nodep->classOrPackagep(vrefp->classOrPackagep()); + return; + } + + // Handle VarXRef: replace VarRef with VarXRef (e.g., nested interface port) + const AstVarXRef* const xrefp = VN_AS(pinExpr, VarXRef); + AstVarXRef* const newp = new AstVarXRef{nodep->fileline(), xrefp->name(), xrefp->dotted(), + nodep->access()}; + newp->varp(xrefp->varp()); + // Copy inlinedDots from pin expression - the normal visitor iteration will + // prepend the cell name when this VarXRef is visited later + newp->inlinedDots(xrefp->inlinedDots()); + nodep->replaceWith(newp); + VL_DO_DANGLING(nodep->deleteTree(), nodep); + // Note: Don't call iterate(newp) here - the node will be visited during + // normal tree iteration which will apply the inlining transformations } void visit(AstVarXRef* nodep) override { // Track what scope it was originally under so V3LinkDot can resolve it @@ -481,13 +496,28 @@ void connectPort(AstNodeModule* modp, AstVar* nodep, AstNodeExpr* pinExprp) { } // Otherwise it must be a variable reference due to having called pinReconnectSimple - const AstVarRef* const pinRefp = VN_AS(pinExprp, VarRef); + const AstNodeVarRef* const pinRefp = VN_AS(pinExprp, NodeVarRef); - // Helper to create an AstVarRef reference to the pin variable - const auto pinRef = [&](VAccess access) { - AstVarRef* const p = new AstVarRef{pinRefp->fileline(), pinRefp->varp(), access}; - p->classOrPackagep(pinRefp->classOrPackagep()); - return p; + const auto pinRefAsVarRef = [&](VAccess access) -> AstVarRef* { + const AstVarRef* const vrp = VN_AS(pinRefp, VarRef); + AstVarRef* const newp = new AstVarRef{vrp->fileline(), vrp->varp(), access}; + newp->classOrPackagep(vrp->classOrPackagep()); + return newp; + }; + + const auto pinRefAsExpr = [&](VAccess access) -> AstNodeExpr* { + if (const AstVarRef* const vrp = VN_CAST(pinRefp, VarRef)) { + AstVarRef* const newp = new AstVarRef{vrp->fileline(), vrp->varp(), access}; + newp->classOrPackagep(vrp->classOrPackagep()); + return newp; + } else { + const AstVarXRef* const xrp = VN_AS(pinRefp, VarXRef); + AstVarXRef* const newp = new AstVarXRef{xrp->fileline(), xrp->name(), xrp->dotted(), + access}; + newp->varp(xrp->varp()); + newp->inlinedDots(xrp->inlinedDots()); + return newp; + } }; // If it is being inlined, create the alias for it @@ -495,10 +525,10 @@ void connectPort(AstNodeModule* modp, AstVar* nodep, AstNodeExpr* pinExprp) { UINFO(6, "Inlining port variable: " << nodep); if (nodep->isIfaceRef()) { modp->addStmtsp( - new AstAliasScope{flp, portRef(VAccess::WRITE), pinRef(VAccess::READ)}); + new AstAliasScope{flp, portRef(VAccess::WRITE), pinRefAsExpr(VAccess::READ)}); } else { AstVarRef* const aliasArgsp = portRef(VAccess::WRITE); - aliasArgsp->addNext(pinRef(VAccess::READ)); + aliasArgsp->addNext(pinRefAsVarRef(VAccess::READ)); modp->addStmtsp(new AstAlias{flp, aliasArgsp}); } // They will become the same variable, so propagate file-line and variable attributes @@ -512,10 +542,12 @@ void connectPort(AstNodeModule* modp, AstVar* nodep, AstNodeExpr* pinExprp) { // Otherwise create the continuous assignment between the port var and the pin expression UINFO(6, "Not inlining port variable: " << nodep); if (nodep->direction() == VDirection::INPUT) { - AstAssignW* const ap = new AstAssignW{flp, portRef(VAccess::WRITE), pinRef(VAccess::READ)}; + AstAssignW* const ap + = new AstAssignW{flp, portRef(VAccess::WRITE), pinRefAsExpr(VAccess::READ)}; modp->addStmtsp(new AstAlways{ap}); } else if (nodep->direction() == VDirection::OUTPUT) { - AstAssignW* const ap = new AstAssignW{flp, pinRef(VAccess::WRITE), portRef(VAccess::READ)}; + AstAssignW* const ap + = new AstAssignW{flp, pinRefAsExpr(VAccess::WRITE), portRef(VAccess::READ)}; modp->addStmtsp(new AstAlways{ap}); } else { pinExprp->v3fatalSrc("V3Tristate left INOUT port"); diff --git a/src/V3LinkDot.cpp b/src/V3LinkDot.cpp index 2f84b2db6..d48d8bda5 100644 --- a/src/V3LinkDot.cpp +++ b/src/V3LinkDot.cpp @@ -777,9 +777,9 @@ public: } else { // Searching for middle submodule, must be a cell name VSymEnt* findSymp = findWithAltFlat(lookupSymp, ident, altIdent); if (!findSymp) findSymp = findForkParentAlias(lookupSymp, ident); - if (findSymp) + if (findSymp) { lookupSymp = unwrapForkParent(findSymp, ident); - else { + } else { return nullptr; // Not found } } @@ -795,6 +795,53 @@ public: } } } + // Follow scope alias for nested interface port access + if (!leftname.empty()) { + const auto aliasIt = m_scopeAliasMap[SAMN_IFTOP].find(lookupSymp); + if (aliasIt != m_scopeAliasMap[SAMN_IFTOP].end()) { + lookupSymp = aliasIt->second; + // Alias may point to __Viftop VarScope; find corresponding Cell + if (const AstVarScope* const vscp + = VN_CAST(lookupSymp->nodep(), VarScope)) { + const string varName = vscp->varp()->name(); + static constexpr const char* const VIFTOP_SUFFIX = "__Viftop"; + if (VString::endsWith(varName, VIFTOP_SUFFIX)) { + const string cellName + = varName.substr(0, varName.size() - strlen(VIFTOP_SUFFIX)); + VSymEnt* const parentSymp = lookupSymp->parentp(); + if (parentSymp) { + VSymEnt* const cellSymp = parentSymp->findIdFlat(cellName); + if (cellSymp && VN_IS(cellSymp->nodep(), Cell)) { + lookupSymp = cellSymp; // Use Cell for member lookup + } + } + } + } + } else { + // No alias; try following IfaceRefDType to interface cell + const AstVar* varp = VN_CAST(lookupSymp->nodep(), Var); + if (!varp) { + if (const AstVarScope* const vscp + = VN_CAST(lookupSymp->nodep(), VarScope)) { + varp = vscp->varp(); + } + } + if (varp && varp->isIfaceRef()) { + if (const AstIfaceRefDType* const ifaceRefp + = ifaceRefFromArray(varp->dtypep())) { + if (ifaceRefp->cellp() && existsNodeSym(ifaceRefp->cellp())) { + lookupSymp = getNodeSym(ifaceRefp->cellp()); + } else if (ifaceRefp->ifaceViaCellp() + && existsNodeSym(ifaceRefp->ifaceViaCellp())) { + lookupSymp = getNodeSym(ifaceRefp->ifaceViaCellp()); + } else if (ifaceRefp->ifacep() + && existsNodeSym(ifaceRefp->ifacep())) { + lookupSymp = getNodeSym(ifaceRefp->ifacep()); + } + } + } + } + } } firstId = false; } @@ -2302,6 +2349,8 @@ class LinkDotScopeVisitor final : public VNVisitor { LinkDotState* const m_statep; // State to pass between visitors, including symbol table const AstScope* m_scopep = nullptr; // The current scope VSymEnt* m_modSymp = nullptr; // Symbol entry for current module + // Deferred AliasScope processing - must be done outer-to-inner for correct alias resolution + std::vector> m_deferredAliasScopes; // METHODS public: @@ -2345,37 +2394,40 @@ private: if (!nodep->varp()->isFuncLocal() && !nodep->varp()->isClassMember()) { VSymEnt* const varSymp = m_statep->insertSym(m_modSymp, nodep->varp()->name(), nodep, nullptr); - if (nodep->varp()->isIfaceRef() && nodep->varp()->isIfaceParent()) { - UINFO(9, "Iface parent ref var " << nodep->varp()->name() << " " << nodep); - // Find the interface cell the var references + if (nodep->varp()->isIfaceRef()) { AstIfaceRefDType* const dtypep = LinkDotState::ifaceRefFromArray(nodep->varp()->dtypep()); UASSERT_OBJ(dtypep, nodep, "Non AstIfaceRefDType on isIfaceRef() var"); - UINFO(9, "Iface parent dtype " << dtypep); - const string ifcellname = dtypep->cellName(); - string baddot; - VSymEnt* okSymp; - VSymEnt* cellSymp = m_statep->findDotted(nodep->fileline(), m_modSymp, ifcellname, - baddot, okSymp, false); - UASSERT_OBJ( - cellSymp, nodep, - "No symbol for interface instance: " << nodep->prettyNameQ(ifcellname)); - UINFO(5, " Found interface instance: se" << cvtToHex(cellSymp) << " " - << cellSymp->nodep()); - if (dtypep->modportName() != "") { - VSymEnt* const mpSymp = m_statep->findDotted( - nodep->fileline(), m_modSymp, ifcellname, baddot, okSymp, false); - UASSERT_OBJ(mpSymp, nodep, - "No symbol for interface modport: " - << nodep->prettyNameQ(dtypep->modportName())); - cellSymp = mpSymp; - UINFO(5, " Found modport cell: se" << cvtToHex(cellSymp) << " " - << mpSymp->nodep()); + if (nodep->varp()->isIfaceParent()) { + UINFO(9, "Iface parent ref var " << nodep->varp()->name() << " " << nodep); + // Find the interface cell the var references + UINFO(9, "Iface parent dtype " << dtypep); + const string ifcellname = dtypep->cellName(); + string baddot; + VSymEnt* okSymp; + VSymEnt* cellSymp + = m_statep->findDotted(nodep->fileline(), m_modSymp, ifcellname, baddot, + okSymp, false); + UASSERT_OBJ( + cellSymp, nodep, + "No symbol for interface instance: " << nodep->prettyNameQ(ifcellname)); + UINFO(5, " Found interface instance: se" << cvtToHex(cellSymp) << " " + << cellSymp->nodep()); + if (dtypep->modportName() != "") { + VSymEnt* const mpSymp = m_statep->findDotted( + nodep->fileline(), m_modSymp, ifcellname, baddot, okSymp, false); + UASSERT_OBJ(mpSymp, nodep, + "No symbol for interface modport: " + << nodep->prettyNameQ(dtypep->modportName())); + cellSymp = mpSymp; + UINFO(5, " Found modport cell: se" << cvtToHex(cellSymp) << " " + << mpSymp->nodep()); + } + // Interface reference; need to put whole thing into + // symtable, but can't clone it now as we may have a later + // alias for it. + m_statep->insertScopeAlias(LinkDotState::SAMN_IFTOP, varSymp, cellSymp); } - // Interface reference; need to put whole thing into - // symtable, but can't clone it now as we may have a later - // alias for it. - m_statep->insertScopeAlias(LinkDotState::SAMN_IFTOP, varSymp, cellSymp); } } } @@ -2414,6 +2466,12 @@ private: pushDeletep(nodep->unlinkFrBack()); } void visit(AstAliasScope* nodep) override { // ScopeVisitor:: + // Defer AliasScope processing - must process outer scopes before inner ones + // so that nested interface port alias resolution works correctly + UINFO(5, "ALIASSCOPE (deferred) " << nodep); + m_deferredAliasScopes.emplace_back(nodep, m_modSymp); + } + void processAliasScope(AstAliasScope* nodep, VSymEnt* modSymp) { UINFO(5, "ALIASSCOPE " << nodep); UINFOTREE(9, nodep, "", "avs"); VSymEnt* rhsSymp; @@ -2425,22 +2483,22 @@ private: string inl = ((xrefp && xrefp->inlinedDots().size()) ? (xrefp->inlinedDots() + "__DOT__") : ""); + const string dottedPath = (xrefp && !xrefp->dotted().empty()) + ? (xrefp->dotted() + ".") + : ""; VSymEnt* symp = nullptr; string scopename; while (!symp) { - scopename - = refp ? refp->name() : (inl.size() ? (inl + xrefp->name()) : xrefp->name()); + scopename = refp ? refp->name() + : (inl.size() ? (inl + dottedPath + xrefp->name()) + : (dottedPath + xrefp->name())); string baddot; VSymEnt* okSymp; - symp = m_statep->findDotted(nodep->rhsp()->fileline(), m_modSymp, scopename, - baddot, okSymp, false); + symp = m_statep->findDotted(nodep->rhsp()->fileline(), modSymp, scopename, + baddot, okSymp, true); if (inl == "") break; inl = LinkDotState::removeLastInlineScope(inl); } - if (!symp) { - UINFO(9, "No symbol for interface alias rhs (" - << std::string{refp ? "VARREF " : "VARXREF "} << scopename << ")"); - } UASSERT_OBJ(symp, nodep, "No symbol for interface alias rhs"); UINFO(5, " Found a linked scope RHS: " << scopename << " se" << cvtToHex(symp) << " " << symp->nodep()); @@ -2457,7 +2515,7 @@ private: = refp ? refp->varp()->name() : xrefp->dotted() + "." + xrefp->name(); string baddot; VSymEnt* okSymp; - VSymEnt* const symp = m_statep->findDotted(nodep->lhsp()->fileline(), m_modSymp, + VSymEnt* const symp = m_statep->findDotted(nodep->lhsp()->fileline(), modSymp, scopename, baddot, okSymp, false); UASSERT_OBJ(symp, nodep, "No symbol for interface alias lhs"); UINFO(5, " Found a linked scope LHS: " << scopename << " se" << cvtToHex(symp) @@ -2470,6 +2528,29 @@ private: // We have stored the link, we don't need these any more VL_DO_DANGLING(nodep->unlinkFrBack()->deleteTree(), nodep); } + void processDeferredAliasScopes() { + // Sort by hierarchy depth (shallower first) so outer aliases are resolved before inner + // Pre-compute depth map to avoid O(N log N * D) complexity in sort comparisons + std::unordered_map depthMap; + for (const auto& pair : m_deferredAliasScopes) { + VSymEnt* const symp = pair.second; + if (depthMap.find(symp) == depthMap.end()) { + int depth = 0; + for (VSymEnt* p = symp; p; p = p->parentp()) ++depth; + depthMap[symp] = depth; + } + } + std::stable_sort(m_deferredAliasScopes.begin(), m_deferredAliasScopes.end(), + [&depthMap](const std::pair& a, + const std::pair& b) { + return depthMap.at(a.second) < depthMap.at(b.second); + }); + // Process in sorted order + for (auto& pair : m_deferredAliasScopes) { + processAliasScope(pair.first, pair.second); + } + m_deferredAliasScopes.clear(); + } void visit(AstNodeGen* nodep) override { // ScopeVisitor:: // LCOV_EXCL_LINE nodep->v3fatalSrc("Generate constructs should have been reduced out"); } @@ -2485,6 +2566,8 @@ public: : m_statep{statep} { UINFO(4, __FUNCTION__ << ": "); iterate(rootp); + // Process deferred AliasScopes in outer-to-inner order + processDeferredAliasScopes(); } ~LinkDotScopeVisitor() override = default; }; @@ -4340,10 +4423,10 @@ class LinkDotResolveVisitor final : public VNVisitor { << okSymp->cellErrorScopes(nodep)); return; } - // V3Inst may have expanded arrays of interfaces to - // AstVarXRef's even though they are in the same module detect - // this and convert to normal VarRefs - if (!m_statep->forPrearray() && !m_statep->forScopeCreation()) { + // V3Inst may have expanded arrays of interfaces to AstVarXRef's even though + // they are in the same module; convert to normal VarRefs (but not if dotted) + if (!m_statep->forPrearray() && !m_statep->forScopeCreation() + && nodep->dotted().empty()) { if (const AstIfaceRefDType* const ifaceDtp = VN_CAST(nodep->dtypep(), IfaceRefDType)) { if (!ifaceDtp->isVirtual()) { diff --git a/src/V3Param.cpp b/src/V3Param.cpp index 8a9c23d98..e257a38a9 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -616,6 +616,91 @@ class ParamProcessor final { if (nodep->op4p()) replaceRefsRecurse(nodep->op4p(), oldClassp, newClassp); if (nodep->nextp()) replaceRefsRecurse(nodep->nextp(), oldClassp, newClassp); } + + // Helper visitor to update VarXRefs to use variables from specialized interfaces. + // When a module with interface ports is cloned and the port's interface is remapped + // to a specialized version, VarXRefs that access members of the old interface need + // to be updated to reference the corresponding members in the new interface. + class VarXRefRelinkVisitor final : public VNVisitor { + AstNodeModule* m_modp; // The cloned module we're updating + std::unordered_map m_varModuleMap; // Cache var->module lookups + + public: + explicit VarXRefRelinkVisitor(AstNodeModule* newModp) + : m_modp{newModp} { + iterate(newModp); + } + + private: + // Find which module a variable belongs to, using cache to avoid repeated backp() walks + AstNodeModule* findVarModule(AstVar* varp) { + const auto it = m_varModuleMap.find(varp); + if (it != m_varModuleMap.end()) return it->second; + AstNodeModule* varModp = nullptr; + for (AstNode* np = varp; np; np = np->backp()) { + if (AstNodeModule* const modp = VN_CAST(np, NodeModule)) { + varModp = modp; + break; + } + } + m_varModuleMap[varp] = varModp; + return varModp; + } + + void visit(AstVarXRef* nodep) override { + AstVar* const varp = nodep->varp(); + if (!varp) { iterateChildren(nodep); return; } + + // Get the dotted prefix (port name) from the VarXRef + // dotted() format: "portname" or "portname.subpath" or empty + const string& dotted = nodep->dotted(); + if (dotted.empty()) { iterateChildren(nodep); return; } + + const size_t dotPos = dotted.find('.'); + const string portName + = (dotPos == string::npos) ? dotted : dotted.substr(0, dotPos); + if (portName.empty()) { iterateChildren(nodep); return; } + + // Find the interface port variable in the cloned module + AstVar* portVarp = nullptr; + for (AstNode* stmtp = m_modp->stmtsp(); stmtp; stmtp = stmtp->nextp()) { + if (AstVar* const varChkp = VN_CAST(stmtp, Var)) { + if (varChkp->name() == portName && varChkp->isIfaceRef()) { + portVarp = varChkp; + break; + } + } + } + if (!portVarp) { iterateChildren(nodep); return; } + + // Get the interface module from the port's dtype + AstIfaceRefDType* const irefp = VN_CAST(portVarp->subDTypep(), IfaceRefDType); + if (!irefp) { iterateChildren(nodep); return; } + + AstNodeModule* const newIfacep = irefp->ifaceViaCellp(); + if (!newIfacep) { iterateChildren(nodep); return; } + + // Find which module the variable currently belongs to (cached) + AstNodeModule* const varModp = findVarModule(varp); + + // If variable is in a different module than the port's interface, remap it + if (varModp && varModp != newIfacep) { + for (AstNode* stmtp = newIfacep->stmtsp(); stmtp; stmtp = stmtp->nextp()) { + if (AstVar* const newVarp = VN_CAST(stmtp, Var)) { + if (newVarp->name() == varp->name()) { + UINFO(9, "VarXRef relink " << varp->name() << " in " + << varModp->name() << " -> " << newIfacep->name() << endl); + nodep->varp(newVarp); + break; + } + } + } + } + iterateChildren(nodep); + } + void visit(AstNode* nodep) override { iterateChildren(nodep); } + }; + // Return true on success, false on error bool deepCloneModule(AstNodeModule* srcModp, AstNode* ifErrorp, AstPin* paramsp, const string& newname, const IfaceRefRefs& ifaceRefRefs) { @@ -737,7 +822,25 @@ class ParamProcessor final { // thus we need to stash this info. collectPins(clonemapp, newModp, srcModp->user3p()); // Relink parameter vars to the new module - relinkPins(clonemapp, paramsp); + // For interface ports (e.g., l3_if #(W, L0A_W) l3), the parameter pins may + // reference variables from the enclosing module rather than from the interface + // being cloned. In such cases, use relinkPinsByName to match by variable name. + // Check if any parameter pins reference variables outside the cloned interface. + // This is O(n) but acceptable since parameter pin lists are typically small (<10 pins). + bool needRelinkByName = false; + if (paramsp) { + for (AstPin* pinp = paramsp; pinp; pinp = VN_AS(pinp->nextp(), Pin)) { + if (pinp->modVarp() && clonemapp->find(pinp->modVarp()) == clonemapp->end()) { + needRelinkByName = true; + break; + } + } + } + if (needRelinkByName) { + relinkPinsByName(paramsp, newModp); + } else { + relinkPins(clonemapp, paramsp); + } // Fix any interface references for (auto it = ifaceRefRefs.cbegin(); it != ifaceRefRefs.cend(); ++it) { @@ -751,6 +854,12 @@ class ParamProcessor final { cloneIrefp->ifacep(pinIrefp->ifaceViaCellp()); UINFO(8, " IfaceNew " << cloneIrefp); } + + // Fix VarXRefs that reference variables in old interfaces. + // Now that interface port dtypes have been updated above, we can use them + // to find the correct interface for each VarXRef. + if (!ifaceRefRefs.empty()) { VarXRefRelinkVisitor{newModp}; } + // 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)) { @@ -962,7 +1071,8 @@ class ParamProcessor final { AstIfaceRefDType* pinIrefp = nullptr; const AstNode* const exprp = pinp->exprp(); const AstVar* const varp - = (exprp && VN_IS(exprp, VarRef)) ? VN_AS(exprp, VarRef)->varp() : nullptr; + = (exprp && VN_IS(exprp, NodeVarRef)) ? VN_AS(exprp, NodeVarRef)->varp() + : nullptr; if (varp && varp->subDTypep() && VN_IS(varp->subDTypep(), IfaceRefDType)) { pinIrefp = VN_AS(varp->subDTypep(), IfaceRefDType); } else if (varp && varp->subDTypep() && arraySubDTypep(varp->subDTypep()) @@ -978,6 +1088,13 @@ class ParamProcessor final { pinIrefp = VN_AS(arraySubDTypep(VN_AS(exprp->op1p(), VarRef)->varp()->subDTypep()), IfaceRefDType); + } else if (VN_IS(exprp, CellArrayRef)) { + // Interface array element selection (e.g., l1(l2.l1[0]) for nested iface array) + // The CellArrayRef is not yet fully linked to an interface type. + // Skip interface cleanup for this pin - V3LinkDot will resolve this later. + // Just continue to the next pin without error. + UINFO(9, "Skipping interface cleanup for CellArrayRef pin: " << pinp << endl); + continue; } UINFO(9, " portIfaceRef " << portIrefp); @@ -1184,9 +1301,11 @@ class ParamProcessor final { cellInterfaceCleanup(pinsp, srcModp, longname /*ref*/, any_overrides /*ref*/, ifaceRefRefs /*ref*/); - // Default params are resolved as overrides + // Classes/modules with type parameters need specialization even when types match defaults. + // This is required for UVM parameterized classes. However, interfaces should NOT + // be specialized when type params match defaults (needed for nested interface ports). bool defaultsResolved = false; - if (!any_overrides) { + if (!any_overrides && !VN_IS(srcModp, Iface)) { for (AstPin* pinp = paramsp; pinp; pinp = VN_AS(pinp->nextp(), Pin)) { if (pinp->modPTypep()) { any_overrides = true; @@ -1238,6 +1357,7 @@ class ParamProcessor final { << " cellName=" << nodep->name() << " cloned=" << cloned); + // Link source class to its specialized version for later relinking of method references if (defaultsResolved) srcModp->user4p(newModp); for (auto* stmtp = newModp->stmtsp(); stmtp; stmtp = stmtp->nextp()) { @@ -1569,6 +1689,26 @@ class ParamVisitor final : public VNVisitor { return false; } + // Recursively specialize nested interface cells within a specialized interface. + // This handles parameter passthrough for nested interface hierarchies. + void specializeNestedIfaceCells(AstNodeModule* ifaceModp) { + for (AstNode* stmtp = ifaceModp->stmtsp(); stmtp; stmtp = stmtp->nextp()) { + AstCell* const nestedCellp = VN_CAST(stmtp, Cell); + if (!nestedCellp) continue; + if (!VN_IS(nestedCellp->modp(), Iface)) continue; + if (!nestedCellp->paramsp()) continue; + if (cellParamsReferenceIfacePorts(nestedCellp)) continue; + + AstNodeModule* const nestedSrcModp = nestedCellp->modp(); + if (AstNodeModule* const nestedNewModp + = m_processor.nodeDeparam(nestedCellp, nestedSrcModp, ifaceModp, + ifaceModp->someInstanceName())) { + // Recursively process nested interfaces within this nested interface + if (nestedNewModp != nestedSrcModp) specializeNestedIfaceCells(nestedNewModp); + } + } + } + // A generic visitor for cells and class refs void visitCellOrClassRef(AstNode* nodep, bool isIface) { // Must do ifaces first, so push to list and do in proper order @@ -1581,7 +1721,13 @@ class ParamVisitor final : public VNVisitor { AstCell* const cellp = VN_CAST(nodep, Cell); if (!cellParamsReferenceIfacePorts(cellp)) { AstNodeModule* const srcModp = cellp->modp(); - m_processor.nodeDeparam(cellp, srcModp, m_modp, m_modp->someInstanceName()); + 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); + } } } @@ -1808,13 +1954,25 @@ class ParamVisitor final : public VNVisitor { V3Const::constifyParamsEdit(nodep->selp()); if (const AstConst* const constp = VN_CAST(nodep->selp(), Const)) { const string index = AstNode::encodeNumber(constp->toSInt()); - const string replacestr = nodep->name() + "__BRA__??__KET__"; + // For nested interface array ports, the node name may have a __Viftop suffix + // that doesn't exist in the original unlinked text. Try without the suffix. + const string viftopSuffix = "__Viftop"; + const string baseName = VString::endsWith(nodep->name(), viftopSuffix) + ? nodep->name().substr(0, nodep->name().size() + - viftopSuffix.size()) + : nodep->name(); + const string replacestr = baseName + "__BRA__??__KET__"; const size_t pos = m_unlinkedTxt.find(replacestr); - UASSERT_OBJ(pos != string::npos, nodep, - "Could not find array index in unlinked text: '" - << m_unlinkedTxt << "' for node: " << nodep); + // For interface port array element selections (e.g., l1(l2.l1[0])), + // the AstCellArrayRef may be visited outside of an AstUnlinkedRef context. + // In such cases, m_unlinkedTxt won't contain the expected pattern. + // Simply skip the replacement - the cell array ref will be resolved later. + if (pos == string::npos) { + UINFO(9, "Skipping unlinked text replacement for " << nodep << endl); + return; + } m_unlinkedTxt.replace(pos, replacestr.length(), - nodep->name() + "__BRA__" + index + "__KET__"); + baseName + "__BRA__" + index + "__KET__"); } else { nodep->v3error("Could not expand constant selection inside dotted reference: " << nodep->selp()->prettyNameQ()); diff --git a/test_regress/t/t_inline_varxref_inlineddots.py b/test_regress/t/t_inline_varxref_inlineddots.py new file mode 100644 index 000000000..8a938befd --- /dev/null +++ b/test_regress/t/t_inline_varxref_inlineddots.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('simulator') + +test.compile() + +test.execute() + +test.passes() diff --git a/test_regress/t/t_inline_varxref_inlineddots.v b/test_regress/t/t_inline_varxref_inlineddots.v new file mode 100644 index 000000000..0a4c5ee6e --- /dev/null +++ b/test_regress/t/t_inline_varxref_inlineddots.v @@ -0,0 +1,42 @@ +// DESCRIPTION: Verilator: VarXRef inlinedDots propagation regression +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Leela Pakanati +// SPDX-License-Identifier: CC0-1.0 + +module src #(parameter [3:0] VAL = 4'h0) (output logic [3:0] val); + /*verilator no_inline_module*/ + assign val = VAL; +endmodule + +module inner (input logic [3:0] in, output logic [3:0] out); + /*verilator inline_module*/ + assign out = in; +endmodule + +module outer #(parameter [3:0] VAL = 4'h0) (output logic [3:0] out); + /*verilator inline_module*/ + logic [3:0] s_val; + src #(.VAL(VAL)) s (.val(s_val)); + // Use hierarchical ref s.val (not s_val) to test inlinedDots propagation + inner u (.in(s.val), .out(out)); +endmodule + +module t; + logic [3:0] out0; + logic [3:0] out1; + logic [3:0] unused; + + // Top-level instance with the same name as the inlined one. + src #(.VAL(4'hF)) s (.val(unused)); + + outer #(.VAL(4'h1)) o0 (.out(out0)); + outer #(.VAL(4'h2)) o1 (.out(out1)); + + initial begin + if (out0 !== 4'h1) $stop; + if (out1 !== 4'h2) $stop; + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_interface_nested_port.py b/test_regress/t/t_interface_nested_port.py new file mode 100755 index 000000000..46d1fe4c0 --- /dev/null +++ b/test_regress/t/t_interface_nested_port.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('simulator') + +test.compile(verilator_flags2=['--binary']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_interface_nested_port.v b/test_regress/t/t_interface_nested_port.v new file mode 100644 index 000000000..6918a5fcb --- /dev/null +++ b/test_regress/t/t_interface_nested_port.v @@ -0,0 +1,297 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Leela Pakanati +// SPDX-License-Identifier: CC0-1.0 + +// Issue #5066 - Combined test for nested interface ports with parameters +// +// Tests all parameter patterns in a 5-deep hierarchy: +// - Derived: W doubles at each level (L3=L4*2, L2A=L3*2, L1=L2*2) +// - Hard-coded: L2B.W=8 regardless of parent +// - Passthrough: L0A_W flows unchanged from top to L0A +// - Default: L0B uses default W=8 +// +// With TOP_W=4, L0A_W=16: +// L4(W=4) -> L3(W=8) -> L2A(W=16) -> L1(W=32) -> L0A(W=16), L0B(W=8) +// -> L2B(W=8) -> L1(W=16) -> L0A(W=16), L0B(W=8) + +interface l0_if #(parameter int W = 8); + logic [W-1:0] tb_in; + logic [W-1:0] dut_out; +endinterface + +interface l1_if #(parameter int W = 8, parameter int L0A_W = 8); + logic [W-1:0] tb_in; + logic [W-1:0] dut_out; + l0_if #(L0A_W) l0a(); // passthrough + l0_if l0b(); // default +endinterface + +interface l2_if #(parameter int W = 8, parameter int L0A_W = 8); + logic [W-1:0] tb_in; + logic [W-1:0] dut_out; + l1_if #(W*2, L0A_W) l1(); // derived +endinterface + +interface l3_if #(parameter int W = 8, parameter int L0A_W = 8); + logic [W-1:0] tb_in; + logic [W-1:0] dut_out; + l2_if #(W*2, L0A_W) l2a(); // derived + l2_if #(8, L0A_W) l2b(); // hard-coded +endinterface + +interface l4_if #(parameter int W = 8, parameter int L0A_W = 8); + logic [W-1:0] tb_in; + logic [W-1:0] dut_out; + l3_if #(W*2, L0A_W) l3(); // derived +endinterface + +// Handlers use unparameterized interface ports with parameterized output widths + +module l0_handler #(parameter int W = 8)( + input logic clk, + l0_if l0, + output logic [W-1:0] dout +); + always_ff @(posedge clk) l0.dut_out <= l0.tb_in ^ W'('1); + assign dout = l0.dut_out; +endmodule + +module l1_reader #(parameter int W = 8)( + l1_if l1, + output logic [W-1:0] dout +); + assign dout = l1.dut_out; +endmodule + +module l1_driver #(parameter int W = 8)( + input logic clk, + l1_if l1 +); + always_ff @(posedge clk) l1.dut_out <= l1.tb_in ^ W'('1); +endmodule + +module l1_handler #(parameter int W = 8, parameter int L0A_W = 8)( + input logic clk, + l1_if l1, + output logic [W-1:0] l1_dout, + output logic [L0A_W-1:0] l0a_dout, + output logic [7:0] l0b_dout +); + // Use reader/driver submodules instead of direct access + l1_reader #(W) m_rdr (.l1(l1), .dout(l1_dout)); + l1_driver #(W) m_drv (.clk(clk), .l1(l1)); + + // Still instantiate l0_handlers for nested ports + l0_handler #(L0A_W) m_l0a (.clk(clk), .l0(l1.l0a), .dout(l0a_dout)); + l0_handler #(8) m_l0b (.clk(clk), .l0(l1.l0b), .dout(l0b_dout)); +endmodule + +module l2_handler #(parameter int W = 8, parameter int L0A_W = 8)( + input logic clk, + l2_if l2, + output logic [W-1:0] l2_dout, + output logic [W*2-1:0] l1_dout, + output logic [L0A_W-1:0] l0a_dout, + output logic [7:0] l0b_dout +); + always_ff @(posedge clk) l2.dut_out <= l2.tb_in ^ W'('1); + assign l2_dout = l2.dut_out; + l1_handler #(W*2, L0A_W) m_l1 ( + .clk(clk), .l1(l2.l1), + .l1_dout(l1_dout), .l0a_dout(l0a_dout), .l0b_dout(l0b_dout) + ); +endmodule + +module l3_reader #(parameter int W = 8)( + l3_if l3, + output logic [W-1:0] dout +); + assign dout = l3.dut_out; +endmodule + +module l3_driver #(parameter int W = 8)( + input logic clk, + l3_if l3 +); + always_ff @(posedge clk) l3.dut_out <= l3.tb_in ^ W'('1); +endmodule + +module l3_handler #(parameter int W = 8, parameter int L0A_W = 8)( + input logic clk, + l3_if l3, + output logic [W-1:0] l3_dout, + output logic [W*2-1:0] l2a_dout, + output logic [W*4-1:0] l1_2a_dout, + output logic [L0A_W-1:0] l0a_2a_dout, + output logic [7:0] l0b_2a_dout, + output logic [7:0] l2b_dout, + output logic [15:0] l1_2b_dout, + output logic [L0A_W-1:0] l0a_2b_dout, + output logic [7:0] l0b_2b_dout +); + // Use reader/driver submodules instead of direct access + l3_reader #(W) m_rdr (.l3(l3), .dout(l3_dout)); + l3_driver #(W) m_drv (.clk(clk), .l3(l3)); + + // Still instantiate l2_handlers for nested ports + l2_handler #(W*2, L0A_W) m_l2a ( + .clk(clk), .l2(l3.l2a), + .l2_dout(l2a_dout), .l1_dout(l1_2a_dout), + .l0a_dout(l0a_2a_dout), .l0b_dout(l0b_2a_dout) + ); + l2_handler #(8, L0A_W) m_l2b ( + .clk(clk), .l2(l3.l2b), + .l2_dout(l2b_dout), .l1_dout(l1_2b_dout), + .l0a_dout(l0a_2b_dout), .l0b_dout(l0b_2b_dout) + ); +endmodule + +module l4_handler #(parameter int W = 8, parameter int L0A_W = 8)( + input logic clk, + l4_if l4, + output logic [W-1:0] l4_dout, + output logic [W*2-1:0] l3_dout, + output logic [W*4-1:0] l2a_dout, + output logic [W*8-1:0] l1_2a_dout, + output logic [L0A_W-1:0] l0a_2a_dout, + output logic [7:0] l0b_2a_dout, + output logic [7:0] l2b_dout, + output logic [15:0] l1_2b_dout, + output logic [L0A_W-1:0] l0a_2b_dout, + output logic [7:0] l0b_2b_dout +); + always_ff @(posedge clk) l4.dut_out <= l4.tb_in ^ W'('1); + assign l4_dout = l4.dut_out; + l3_handler #(W*2, L0A_W) m_l3 ( + .clk(clk), .l3(l4.l3), + .l3_dout(l3_dout), + .l2a_dout(l2a_dout), .l1_2a_dout(l1_2a_dout), + .l0a_2a_dout(l0a_2a_dout), .l0b_2a_dout(l0b_2a_dout), + .l2b_dout(l2b_dout), .l1_2b_dout(l1_2b_dout), + .l0a_2b_dout(l0a_2b_dout), .l0b_2b_dout(l0b_2b_dout) + ); +endmodule + +module t; + logic clk = 0; + int cyc = 0; + + localparam int TOP_W = 4; + localparam int L0A_W = 16; + + l4_if #(TOP_W, L0A_W) inst(); + + logic [TOP_W-1:0] l4_dout; + logic [TOP_W*2-1:0] l3_dout; + logic [TOP_W*4-1:0] l2a_dout; + logic [TOP_W*8-1:0] l1_2a_dout; + logic [L0A_W-1:0] l0a_2a_dout; + logic [7:0] l0b_2a_dout; + logic [7:0] l2b_dout; + logic [15:0] l1_2b_dout; + logic [L0A_W-1:0] l0a_2b_dout; + logic [7:0] l0b_2b_dout; + + l4_handler #(TOP_W, L0A_W) m_l4 ( + .clk(clk), .l4(inst), + .l4_dout(l4_dout), + .l3_dout(l3_dout), + .l2a_dout(l2a_dout), .l1_2a_dout(l1_2a_dout), + .l0a_2a_dout(l0a_2a_dout), .l0b_2a_dout(l0b_2a_dout), + .l2b_dout(l2b_dout), .l1_2b_dout(l1_2b_dout), + .l0a_2b_dout(l0a_2b_dout), .l0b_2b_dout(l0b_2b_dout) + ); + + always #5 clk = ~clk; + + always_ff @(posedge clk) begin + inst.tb_in <= cyc[TOP_W-1:0]; + inst.l3.tb_in <= cyc[TOP_W*2-1:0] + (TOP_W*2)'(1); + inst.l3.l2a.tb_in <= cyc[TOP_W*4-1:0] + (TOP_W*4)'(2); + inst.l3.l2a.l1.tb_in <= cyc[TOP_W*8-1:0] + (TOP_W*8)'(3); + inst.l3.l2a.l1.l0a.tb_in <= cyc[L0A_W-1:0] + L0A_W'(4); + inst.l3.l2a.l1.l0b.tb_in <= cyc[7:0] + 8'd5; + inst.l3.l2b.tb_in <= cyc[7:0] + 8'd6; + inst.l3.l2b.l1.tb_in <= cyc[15:0] + 16'd7; + inst.l3.l2b.l1.l0a.tb_in <= cyc[L0A_W-1:0] + L0A_W'(8); + inst.l3.l2b.l1.l0b.tb_in <= cyc[7:0] + 8'd9; + end + + logic [TOP_W-1:0] exp_l4; + logic [TOP_W*2-1:0] exp_l3; + logic [TOP_W*4-1:0] exp_l2a; + logic [TOP_W*8-1:0] exp_l1_2a; + logic [L0A_W-1:0] exp_l0a_2a; + logic [7:0] exp_l0b_2a; + logic [7:0] exp_l2b; + logic [15:0] exp_l1_2b; + logic [L0A_W-1:0] exp_l0a_2b; + logic [7:0] exp_l0b_2b; + + always_ff @(posedge clk) begin + exp_l4 <= inst.tb_in ^ TOP_W'('1); + exp_l3 <= inst.l3.tb_in ^ (TOP_W*2)'('1); + exp_l2a <= inst.l3.l2a.tb_in ^ (TOP_W*4)'('1); + exp_l1_2a <= inst.l3.l2a.l1.tb_in ^ (TOP_W*8)'('1); + exp_l0a_2a <= inst.l3.l2a.l1.l0a.tb_in ^ L0A_W'('1); + exp_l0b_2a <= inst.l3.l2a.l1.l0b.tb_in ^ 8'hFF; + exp_l2b <= inst.l3.l2b.tb_in ^ 8'hFF; + exp_l1_2b <= inst.l3.l2b.l1.tb_in ^ 16'hFFFF; + exp_l0a_2b <= inst.l3.l2b.l1.l0a.tb_in ^ L0A_W'('1); + exp_l0b_2b <= inst.l3.l2b.l1.l0b.tb_in ^ 8'hFF; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + + if (cyc > 3) begin + if (l4_dout !== exp_l4) begin + $display("FAIL cyc=%0d: l4_dout=%h expected %h", cyc, l4_dout, exp_l4); + $stop; + end + if (l3_dout !== exp_l3) begin + $display("FAIL cyc=%0d: l3_dout=%h expected %h", cyc, l3_dout, exp_l3); + $stop; + end + if (l2a_dout !== exp_l2a) begin + $display("FAIL cyc=%0d: l2a_dout=%h expected %h", cyc, l2a_dout, exp_l2a); + $stop; + end + if (l1_2a_dout !== exp_l1_2a) begin + $display("FAIL cyc=%0d: l1_2a_dout=%h expected %h", cyc, l1_2a_dout, exp_l1_2a); + $stop; + end + if (l0a_2a_dout !== exp_l0a_2a) begin + $display("FAIL cyc=%0d: l0a_2a_dout=%h expected %h", cyc, l0a_2a_dout, exp_l0a_2a); + $stop; + end + if (l0b_2a_dout !== exp_l0b_2a) begin + $display("FAIL cyc=%0d: l0b_2a_dout=%h expected %h", cyc, l0b_2a_dout, exp_l0b_2a); + $stop; + end + if (l2b_dout !== exp_l2b) begin + $display("FAIL cyc=%0d: l2b_dout=%h expected %h", cyc, l2b_dout, exp_l2b); + $stop; + end + if (l1_2b_dout !== exp_l1_2b) begin + $display("FAIL cyc=%0d: l1_2b_dout=%h expected %h", cyc, l1_2b_dout, exp_l1_2b); + $stop; + end + if (l0a_2b_dout !== exp_l0a_2b) begin + $display("FAIL cyc=%0d: l0a_2b_dout=%h expected %h", cyc, l0a_2b_dout, exp_l0a_2b); + $stop; + end + if (l0b_2b_dout !== exp_l0b_2b) begin + $display("FAIL cyc=%0d: l0b_2b_dout=%h expected %h", cyc, l0b_2b_dout, exp_l0b_2b); + $stop; + end + end + + if (cyc == 20) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end +endmodule diff --git a/test_regress/t/t_interface_nested_port_array.out b/test_regress/t/t_interface_nested_port_array.out new file mode 100644 index 000000000..0dfa47671 --- /dev/null +++ b/test_regress/t/t_interface_nested_port_array.out @@ -0,0 +1,24 @@ +%Error: t/t_interface_nested_port_array.v:111:3: Interface 'l3_if' not connected as parent's interface not connected + : ... note: In instance 't.m_l3' + : ... Perhaps caused by another error on the parent interface that needs resolving + : ... Or, perhaps intended an interface instantiation but are missing parenthesis (IEEE 1800-2023 25.3)? + 111 | l3_if #(W, L0A_W) l3, + | ^~~~~ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Error: t/t_interface_nested_port_array.v:83:3: Interface 'l2_if' not connected as parent's interface not connected + : ... note: In instance 't.m_l3.m_l2' + : ... Perhaps caused by another error on the parent interface that needs resolving + : ... Or, perhaps intended an interface instantiation but are missing parenthesis (IEEE 1800-2023 25.3)? + 83 | l2_if #(W, L0A_W) l2s[1:0], + | ^~~~~ +%Error: t/t_interface_nested_port_array.v:60:3: Interface 'l2_if' not connected as parent's interface not connected + : ... note: In instance 't.m_l3.m_l2.m_l2b' + : ... Perhaps caused by another error on the parent interface that needs resolving + : ... Or, perhaps intended an interface instantiation but are missing parenthesis (IEEE 1800-2023 25.3)? + 60 | l2_if #(W, L0A_W) l2, + | ^~~~~ +%Error: Internal Error: t/t_interface_nested_port_array.v:22:11: ../V3LinkDot.cpp:#: Module/etc never assigned a symbol entry? + : ... note: In instance 't.m_l3.m_l2.m_l2b.m_l1_1' + 22 | interface l2_if #(parameter int W = 8, parameter int L0A_W = 8); + | ^~~~~ + ... This fatal error may be caused by the earlier error(s); resolve those first. diff --git a/test_regress/t/t_interface_nested_port_array.py b/test_regress/t/t_interface_nested_port_array.py new file mode 100755 index 000000000..e30519452 --- /dev/null +++ b/test_regress/t/t_interface_nested_port_array.py @@ -0,0 +1,22 @@ +#!/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('linter') + +# Issue #5066: Nested interface ports through interface arrays +# (e.g., l2.l1[0] where l1 is an interface array inside interface l2). +# V3Param internal errors have been fixed, but V3LinkDot interface +# connection resolution for array element selections is not yet implemented. +# This test documents the current behavior and should be updated when +# full support is added. +test.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_interface_nested_port_array.v b/test_regress/t/t_interface_nested_port_array.v new file mode 100644 index 000000000..0b9bd0a91 --- /dev/null +++ b/test_regress/t/t_interface_nested_port_array.v @@ -0,0 +1,279 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Leela Pakanati +// SPDX-License-Identifier: CC0-1.0 + +// Issue #5066 - Nested interface ports through interface arrays +// Similar structure to t_interface_nested_port.v, but with interface arrays. + +interface l0_if #(parameter int W = 8); + logic [W-1:0] tb_in; + logic [W-1:0] dut_out; +endinterface + +interface l1_if #(parameter int W = 8, parameter int L0A_W = 8); + logic [W-1:0] tb_in; + logic [W-1:0] dut_out; + l0_if #(L0A_W) l0a[1:0](); // arrayed passthrough + l0_if l0b(); // default +endinterface + +interface l2_if #(parameter int W = 8, parameter int L0A_W = 8); + logic [W-1:0] tb_in; + logic [W-1:0] dut_out; + l1_if #(W*2, L0A_W) l1[1:0](); // derived +endinterface + +interface l3_if #(parameter int W = 8, parameter int L0A_W = 8); + logic [W-1:0] tb_in; + logic [W-1:0] dut_out; + l2_if #(W*2, L0A_W) l2[1:0](); // arrayed +endinterface + +module l0_handler #(parameter int W = 8)( + input logic clk, + l0_if #(W) l0, + output logic [W-1:0] dout +); + always_ff @(posedge clk) l0.dut_out <= l0.tb_in ^ W'('1); + assign dout = l0.dut_out; +endmodule + +module l1_handler #(parameter int W = 8, parameter int L0A_W = 8)( + input logic clk, + l1_if #(W, L0A_W) l1, + output logic [W-1:0] l1_dout, + output logic [L0A_W-1:0] l0a0_dout, + output logic [L0A_W-1:0] l0a1_dout, + output logic [7:0] l0b_dout +); + always_ff @(posedge clk) l1.dut_out <= l1.tb_in ^ W'('1); + assign l1_dout = l1.dut_out; + l0_handler #(L0A_W) m_l0a0 (.clk(clk), .l0(l1.l0a[0]), .dout(l0a0_dout)); + l0_handler #(L0A_W) m_l0a1 (.clk(clk), .l0(l1.l0a[1]), .dout(l0a1_dout)); + l0_handler #(8) m_l0b (.clk(clk), .l0(l1.l0b), .dout(l0b_dout)); +endmodule + +module l2_handler #(parameter int W = 8, parameter int L0A_W = 8)( + input logic clk, + l2_if #(W, L0A_W) l2, + output logic [W-1:0] l2_dout, + output logic [W*2-1:0] l1_0_dout, + output logic [L0A_W-1:0] l0a0_0_dout, + output logic [L0A_W-1:0] l0a1_1_dout, + output logic [7:0] l0b_1_dout +); + always_ff @(posedge clk) l2.dut_out <= l2.tb_in ^ W'('1); + assign l2_dout = l2.dut_out; + l1_handler #(W*2, L0A_W) m_l1_0 ( + .clk(clk), .l1(l2.l1[0]), + .l1_dout(l1_0_dout), .l0a0_dout(l0a0_0_dout), + .l0a1_dout(), .l0b_dout() + ); + l1_handler #(W*2, L0A_W) m_l1_1 ( + .clk(clk), .l1(l2.l1[1]), + .l1_dout(), .l0a0_dout(), + .l0a1_dout(l0a1_1_dout), .l0b_dout(l0b_1_dout) + ); +endmodule + +module l2_array_handler #(parameter int W = 8, parameter int L0A_W = 8)( + input logic clk, + l2_if #(W, L0A_W) l2s[1:0], + output logic [W-1:0] l2a_dout, + output logic [W*2-1:0] l2a_l1_0_dout, + output logic [L0A_W-1:0] l2a_l0a0_0_dout, + output logic [L0A_W-1:0] l2a_l0a1_1_dout, + output logic [7:0] l2a_l0b_1_dout, + output logic [W-1:0] l2b_dout, + output logic [W*2-1:0] l2b_l1_0_dout, + output logic [L0A_W-1:0] l2b_l0a0_0_dout, + output logic [L0A_W-1:0] l2b_l0a1_1_dout, + output logic [7:0] l2b_l0b_1_dout +); + l2_handler #(W, L0A_W) m_l2a ( + .clk(clk), .l2(l2s[0]), + .l2_dout(l2a_dout), + .l1_0_dout(l2a_l1_0_dout), .l0a0_0_dout(l2a_l0a0_0_dout), + .l0a1_1_dout(l2a_l0a1_1_dout), .l0b_1_dout(l2a_l0b_1_dout) + ); + l2_handler #(W, L0A_W) m_l2b ( + .clk(clk), .l2(l2s[1]), + .l2_dout(l2b_dout), + .l1_0_dout(l2b_l1_0_dout), .l0a0_0_dout(l2b_l0a0_0_dout), + .l0a1_1_dout(l2b_l0a1_1_dout), .l0b_1_dout(l2b_l0b_1_dout) + ); +endmodule + +module l3_handler #(parameter int W = 8, parameter int L0A_W = 8)( + input logic clk, + l3_if #(W, L0A_W) l3, + output logic [W-1:0] l3_dout, + output logic [W*2-1:0] l2a_dout, + output logic [W*4-1:0] l2a_l1_0_dout, + output logic [L0A_W-1:0] l2a_l0a0_0_dout, + output logic [L0A_W-1:0] l2a_l0a1_1_dout, + output logic [7:0] l2a_l0b_1_dout, + output logic [W*2-1:0] l2b_dout, + output logic [W*4-1:0] l2b_l1_0_dout, + output logic [L0A_W-1:0] l2b_l0a0_0_dout, + output logic [L0A_W-1:0] l2b_l0a1_1_dout, + output logic [7:0] l2b_l0b_1_dout +); + always_ff @(posedge clk) l3.dut_out <= l3.tb_in ^ W'('1); + assign l3_dout = l3.dut_out; + l2_array_handler #(W*2, L0A_W) m_l2 ( + .clk(clk), .l2s(l3.l2), + .l2a_dout(l2a_dout), + .l2a_l1_0_dout(l2a_l1_0_dout), .l2a_l0a0_0_dout(l2a_l0a0_0_dout), + .l2a_l0a1_1_dout(l2a_l0a1_1_dout), .l2a_l0b_1_dout(l2a_l0b_1_dout), + .l2b_dout(l2b_dout), + .l2b_l1_0_dout(l2b_l1_0_dout), .l2b_l0a0_0_dout(l2b_l0a0_0_dout), + .l2b_l0a1_1_dout(l2b_l0a1_1_dout), .l2b_l0b_1_dout(l2b_l0b_1_dout) + ); +endmodule + +module t; + logic clk = 0; + int cyc = 0; + + localparam int TOP_W = 4; + localparam int L0A_W = 12; + + l3_if #(TOP_W, L0A_W) inst(); + + logic [TOP_W-1:0] l3_dout; + logic [TOP_W*2-1:0] l2a_dout; + logic [TOP_W*4-1:0] l2a_l1_0_dout; + logic [L0A_W-1:0] l2a_l0a0_0_dout; + logic [L0A_W-1:0] l2a_l0a1_1_dout; + logic [7:0] l2a_l0b_1_dout; + logic [TOP_W*2-1:0] l2b_dout; + logic [TOP_W*4-1:0] l2b_l1_0_dout; + logic [L0A_W-1:0] l2b_l0a0_0_dout; + logic [L0A_W-1:0] l2b_l0a1_1_dout; + logic [7:0] l2b_l0b_1_dout; + + l3_handler #(TOP_W, L0A_W) m_l3 ( + .clk(clk), .l3(inst), + .l3_dout(l3_dout), + .l2a_dout(l2a_dout), + .l2a_l1_0_dout(l2a_l1_0_dout), .l2a_l0a0_0_dout(l2a_l0a0_0_dout), + .l2a_l0a1_1_dout(l2a_l0a1_1_dout), .l2a_l0b_1_dout(l2a_l0b_1_dout), + .l2b_dout(l2b_dout), + .l2b_l1_0_dout(l2b_l1_0_dout), .l2b_l0a0_0_dout(l2b_l0a0_0_dout), + .l2b_l0a1_1_dout(l2b_l0a1_1_dout), .l2b_l0b_1_dout(l2b_l0b_1_dout) + ); + + always #5 clk = ~clk; + + always_ff @(posedge clk) begin + inst.tb_in <= cyc[TOP_W-1:0]; + + inst.l2[0].tb_in <= cyc[TOP_W*2-1:0] + (TOP_W*2)'(1); + inst.l2[0].l1[0].tb_in <= cyc[TOP_W*4-1:0] + (TOP_W*4)'(2); + inst.l2[0].l1[0].l0a[0].tb_in <= cyc[L0A_W-1:0] + L0A_W'(3); + inst.l2[0].l1[1].l0a[1].tb_in <= cyc[L0A_W-1:0] + L0A_W'(4); + inst.l2[0].l1[1].l0b.tb_in <= cyc[7:0] + 8'd5; + + inst.l2[1].tb_in <= cyc[TOP_W*2-1:0] + (TOP_W*2)'(6); + inst.l2[1].l1[0].tb_in <= cyc[TOP_W*4-1:0] + (TOP_W*4)'(7); + inst.l2[1].l1[0].l0a[0].tb_in <= cyc[L0A_W-1:0] + L0A_W'(8); + inst.l2[1].l1[1].l0a[1].tb_in <= cyc[L0A_W-1:0] + L0A_W'(9); + inst.l2[1].l1[1].l0b.tb_in <= cyc[7:0] + 8'd10; + end + + logic [TOP_W-1:0] exp_l3_dout; + logic [TOP_W*2-1:0] exp_l2a_dout; + logic [TOP_W*4-1:0] exp_l2a_l1_0_dout; + logic [L0A_W-1:0] exp_l2a_l0a0_0_dout; + logic [L0A_W-1:0] exp_l2a_l0a1_1_dout; + logic [7:0] exp_l2a_l0b_1_dout; + logic [TOP_W*2-1:0] exp_l2b_dout; + logic [TOP_W*4-1:0] exp_l2b_l1_0_dout; + logic [L0A_W-1:0] exp_l2b_l0a0_0_dout; + logic [L0A_W-1:0] exp_l2b_l0a1_1_dout; + logic [7:0] exp_l2b_l0b_1_dout; + + always_ff @(posedge clk) begin + exp_l3_dout <= inst.tb_in ^ TOP_W'('1); + + exp_l2a_dout <= inst.l2[0].tb_in ^ (TOP_W*2)'('1); + exp_l2a_l1_0_dout <= inst.l2[0].l1[0].tb_in ^ (TOP_W*4)'('1); + exp_l2a_l0a0_0_dout <= inst.l2[0].l1[0].l0a[0].tb_in ^ L0A_W'('1); + exp_l2a_l0a1_1_dout <= inst.l2[0].l1[1].l0a[1].tb_in ^ L0A_W'('1); + exp_l2a_l0b_1_dout <= inst.l2[0].l1[1].l0b.tb_in ^ 8'hFF; + + exp_l2b_dout <= inst.l2[1].tb_in ^ (TOP_W*2)'('1); + exp_l2b_l1_0_dout <= inst.l2[1].l1[0].tb_in ^ (TOP_W*4)'('1); + exp_l2b_l0a0_0_dout <= inst.l2[1].l1[0].l0a[0].tb_in ^ L0A_W'('1); + exp_l2b_l0a1_1_dout <= inst.l2[1].l1[1].l0a[1].tb_in ^ L0A_W'('1); + exp_l2b_l0b_1_dout <= inst.l2[1].l1[1].l0b.tb_in ^ 8'hFF; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + + if (cyc > 3) begin + if (l3_dout !== exp_l3_dout) begin + $display("FAIL cyc=%0d: l3_dout=%h expected %h", cyc, l3_dout, exp_l3_dout); + $stop; + end + if (l2a_dout !== exp_l2a_dout) begin + $display("FAIL cyc=%0d: l2a_dout=%h expected %h", cyc, l2a_dout, exp_l2a_dout); + $stop; + end + if (l2a_l1_0_dout !== exp_l2a_l1_0_dout) begin + $display("FAIL cyc=%0d: l2a_l1_0_dout=%h expected %h", + cyc, l2a_l1_0_dout, exp_l2a_l1_0_dout); + $stop; + end + if (l2a_l0a0_0_dout !== exp_l2a_l0a0_0_dout) begin + $display("FAIL cyc=%0d: l2a_l0a0_0_dout=%h expected %h", + cyc, l2a_l0a0_0_dout, exp_l2a_l0a0_0_dout); + $stop; + end + if (l2a_l0a1_1_dout !== exp_l2a_l0a1_1_dout) begin + $display("FAIL cyc=%0d: l2a_l0a1_1_dout=%h expected %h", + cyc, l2a_l0a1_1_dout, exp_l2a_l0a1_1_dout); + $stop; + end + if (l2a_l0b_1_dout !== exp_l2a_l0b_1_dout) begin + $display("FAIL cyc=%0d: l2a_l0b_1_dout=%h expected %h", + cyc, l2a_l0b_1_dout, exp_l2a_l0b_1_dout); + $stop; + end + + if (l2b_dout !== exp_l2b_dout) begin + $display("FAIL cyc=%0d: l2b_dout=%h expected %h", cyc, l2b_dout, exp_l2b_dout); + $stop; + end + if (l2b_l1_0_dout !== exp_l2b_l1_0_dout) begin + $display("FAIL cyc=%0d: l2b_l1_0_dout=%h expected %h", + cyc, l2b_l1_0_dout, exp_l2b_l1_0_dout); + $stop; + end + if (l2b_l0a0_0_dout !== exp_l2b_l0a0_0_dout) begin + $display("FAIL cyc=%0d: l2b_l0a0_0_dout=%h expected %h", + cyc, l2b_l0a0_0_dout, exp_l2b_l0a0_0_dout); + $stop; + end + if (l2b_l0a1_1_dout !== exp_l2b_l0a1_1_dout) begin + $display("FAIL cyc=%0d: l2b_l0a1_1_dout=%h expected %h", + cyc, l2b_l0a1_1_dout, exp_l2b_l0a1_1_dout); + $stop; + end + if (l2b_l0b_1_dout !== exp_l2b_l0b_1_dout) begin + $display("FAIL cyc=%0d: l2b_l0b_1_dout=%h expected %h", + cyc, l2b_l0b_1_dout, exp_l2b_l0b_1_dout); + $stop; + end + end + + if (cyc == 20) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end +endmodule diff --git a/test_regress/t/t_interface_nested_port_array_noinl.out b/test_regress/t/t_interface_nested_port_array_noinl.out new file mode 100644 index 000000000..0dfa47671 --- /dev/null +++ b/test_regress/t/t_interface_nested_port_array_noinl.out @@ -0,0 +1,24 @@ +%Error: t/t_interface_nested_port_array.v:111:3: Interface 'l3_if' not connected as parent's interface not connected + : ... note: In instance 't.m_l3' + : ... Perhaps caused by another error on the parent interface that needs resolving + : ... Or, perhaps intended an interface instantiation but are missing parenthesis (IEEE 1800-2023 25.3)? + 111 | l3_if #(W, L0A_W) l3, + | ^~~~~ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Error: t/t_interface_nested_port_array.v:83:3: Interface 'l2_if' not connected as parent's interface not connected + : ... note: In instance 't.m_l3.m_l2' + : ... Perhaps caused by another error on the parent interface that needs resolving + : ... Or, perhaps intended an interface instantiation but are missing parenthesis (IEEE 1800-2023 25.3)? + 83 | l2_if #(W, L0A_W) l2s[1:0], + | ^~~~~ +%Error: t/t_interface_nested_port_array.v:60:3: Interface 'l2_if' not connected as parent's interface not connected + : ... note: In instance 't.m_l3.m_l2.m_l2b' + : ... Perhaps caused by another error on the parent interface that needs resolving + : ... Or, perhaps intended an interface instantiation but are missing parenthesis (IEEE 1800-2023 25.3)? + 60 | l2_if #(W, L0A_W) l2, + | ^~~~~ +%Error: Internal Error: t/t_interface_nested_port_array.v:22:11: ../V3LinkDot.cpp:#: Module/etc never assigned a symbol entry? + : ... note: In instance 't.m_l3.m_l2.m_l2b.m_l1_1' + 22 | interface l2_if #(parameter int W = 8, parameter int L0A_W = 8); + | ^~~~~ + ... This fatal error may be caused by the earlier error(s); resolve those first. diff --git a/test_regress/t/t_interface_nested_port_array_noinl.py b/test_regress/t/t_interface_nested_port_array_noinl.py new file mode 100755 index 000000000..d0884ad33 --- /dev/null +++ b/test_regress/t/t_interface_nested_port_array_noinl.py @@ -0,0 +1,23 @@ +#!/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('linter') +test.top_filename = "t/t_interface_nested_port_array.v" + +# Issue #5066: Nested interface ports through interface arrays +# (e.g., l2.l1[0] where l1 is an interface array inside interface l2). +# V3Param internal errors have been fixed, but V3LinkDot interface +# connection resolution for array element selections is not yet implemented. +# This test documents the current behavior and should be updated when +# full support is added. +test.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_interface_nested_port_noinl.py b/test_regress/t/t_interface_nested_port_noinl.py new file mode 100755 index 000000000..50db1bfbd --- /dev/null +++ b/test_regress/t/t_interface_nested_port_noinl.py @@ -0,0 +1,19 @@ +#!/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('simulator') +test.top_filename = "t/t_interface_nested_port.v" + +test.compile(verilator_flags2=['--binary', '-fno-inline']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_interface_nested_port_type.py b/test_regress/t/t_interface_nested_port_type.py new file mode 100755 index 000000000..46d1fe4c0 --- /dev/null +++ b/test_regress/t/t_interface_nested_port_type.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('simulator') + +test.compile(verilator_flags2=['--binary']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_interface_nested_port_type.v b/test_regress/t/t_interface_nested_port_type.v new file mode 100644 index 000000000..4d9977a88 --- /dev/null +++ b/test_regress/t/t_interface_nested_port_type.v @@ -0,0 +1,297 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Leela Pakanati +// SPDX-License-Identifier: CC0-1.0 + +// Issue #5066 - Test nested interface ports with type parameters +// +// Tests all parameter patterns in a 5-deep hierarchy using types: +// - Derived: Type width doubles at each level (L3=bits(T)*2, etc.) +// - Hard-coded: L2B.T=logic[7:0] regardless of parent +// - Passthrough: L0A_T flows unchanged from top to L0A +// - Default: L0B uses default T=logic[7:0] +// +// With TOP_T=logic[3:0], L0A_T=logic[15:0]: +// L4(T=4b) -> L3(T=8b) -> L2A(T=16b) -> L1(T=32b) -> L0A(T=16b), L0B(T=8b) +// -> L2B(T=8b) -> L1(T=16b) -> L0A(T=16b), L0B(T=8b) + +interface l0_if #(parameter type T = logic [7:0]); + T tb_in; + T dut_out; +endinterface + +interface l1_if #(parameter type T = logic, parameter type L0A_T = logic); + T tb_in; + T dut_out; + l0_if #(.T(L0A_T)) l0a(); // passthrough + l0_if l0b(); // default +endinterface + +interface l2_if #(parameter type T = logic, parameter type L0A_T = logic); + T tb_in; + T dut_out; + l1_if #(.T(logic [$bits(T)*2-1:0]), .L0A_T(L0A_T)) l1(); // derived +endinterface + +interface l3_if #(parameter type T = logic, parameter type L0A_T = logic); + T tb_in; + T dut_out; + l2_if #(.T(logic [$bits(T)*2-1:0]), .L0A_T(L0A_T)) l2a(); // derived + l2_if #(.T(logic [7:0]), .L0A_T(L0A_T)) l2b(); // hard-coded +endinterface + +interface l4_if #(parameter type T = logic, parameter type L0A_T = logic); + T tb_in; + T dut_out; + l3_if #(.T(logic [$bits(T)*2-1:0]), .L0A_T(L0A_T)) l3(); // derived +endinterface + +// Handlers use type parameters with derived output types + +module l0_handler #(parameter type T = logic [7:0])( + input logic clk, + l0_if l0, + output T dout +); + always_ff @(posedge clk) l0.dut_out <= l0.tb_in ^ T'('1); + assign dout = l0.dut_out; +endmodule + +module l1_reader #(parameter type T = logic)( + l1_if l1, + output T dout +); + assign dout = l1.dut_out; +endmodule + +module l1_driver #(parameter type T = logic)( + input logic clk, + l1_if l1 +); + always_ff @(posedge clk) l1.dut_out <= l1.tb_in ^ T'('1); +endmodule + +module l1_handler #(parameter type T = logic, parameter type L0A_T = logic)( + input logic clk, + l1_if l1, + output T l1_dout, + output L0A_T l0a_dout, + output logic [7:0] l0b_dout +); + // Use reader/driver submodules instead of direct access + l1_reader #(.T(T)) m_rdr (.l1(l1), .dout(l1_dout)); + l1_driver #(.T(T)) m_drv (.clk(clk), .l1(l1)); + + // Still instantiate l0_handlers for nested ports + l0_handler #(.T(L0A_T)) m_l0a (.clk(clk), .l0(l1.l0a), .dout(l0a_dout)); + l0_handler #(.T(logic [7:0])) m_l0b (.clk(clk), .l0(l1.l0b), .dout(l0b_dout)); +endmodule + +module l2_handler #(parameter type T = logic, parameter type L0A_T = logic)( + input logic clk, + l2_if l2, + output T l2_dout, + output logic [$bits(T)*2-1:0] l1_dout, + output L0A_T l0a_dout, + output logic [7:0] l0b_dout +); + always_ff @(posedge clk) l2.dut_out <= l2.tb_in ^ T'('1); + assign l2_dout = l2.dut_out; + l1_handler #(.T(logic [$bits(T)*2-1:0]), .L0A_T(L0A_T)) m_l1 ( + .clk(clk), .l1(l2.l1), + .l1_dout(l1_dout), .l0a_dout(l0a_dout), .l0b_dout(l0b_dout) + ); +endmodule + +module l3_reader #(parameter type T = logic)( + l3_if l3, + output T dout +); + assign dout = l3.dut_out; +endmodule + +module l3_driver #(parameter type T = logic)( + input logic clk, + l3_if l3 +); + always_ff @(posedge clk) l3.dut_out <= l3.tb_in ^ T'('1); +endmodule + +module l3_handler #(parameter type T = logic, parameter type L0A_T = logic)( + input logic clk, + l3_if l3, + output T l3_dout, + output logic [$bits(T)*2-1:0] l2a_dout, + output logic [$bits(T)*4-1:0] l1_2a_dout, + output L0A_T l0a_2a_dout, + output logic [7:0] l0b_2a_dout, + output logic [7:0] l2b_dout, + output logic [15:0] l1_2b_dout, + output L0A_T l0a_2b_dout, + output logic [7:0] l0b_2b_dout +); + // Use reader/driver submodules instead of direct access + l3_reader #(.T(T)) m_rdr (.l3(l3), .dout(l3_dout)); + l3_driver #(.T(T)) m_drv (.clk(clk), .l3(l3)); + + // Still instantiate l2_handlers for nested ports + l2_handler #(.T(logic [$bits(T)*2-1:0]), .L0A_T(L0A_T)) m_l2a ( + .clk(clk), .l2(l3.l2a), + .l2_dout(l2a_dout), .l1_dout(l1_2a_dout), + .l0a_dout(l0a_2a_dout), .l0b_dout(l0b_2a_dout) + ); + l2_handler #(.T(logic [7:0]), .L0A_T(L0A_T)) m_l2b ( + .clk(clk), .l2(l3.l2b), + .l2_dout(l2b_dout), .l1_dout(l1_2b_dout), + .l0a_dout(l0a_2b_dout), .l0b_dout(l0b_2b_dout) + ); +endmodule + +module l4_handler #(parameter type T = logic, parameter type L0A_T = logic)( + input logic clk, + l4_if l4, + output T l4_dout, + output logic [$bits(T)*2-1:0] l3_dout, + output logic [$bits(T)*4-1:0] l2a_dout, + output logic [$bits(T)*8-1:0] l1_2a_dout, + output L0A_T l0a_2a_dout, + output logic [7:0] l0b_2a_dout, + output logic [7:0] l2b_dout, + output logic [15:0] l1_2b_dout, + output L0A_T l0a_2b_dout, + output logic [7:0] l0b_2b_dout +); + always_ff @(posedge clk) l4.dut_out <= l4.tb_in ^ T'('1); + assign l4_dout = l4.dut_out; + l3_handler #(.T(logic [$bits(T)*2-1:0]), .L0A_T(L0A_T)) m_l3 ( + .clk(clk), .l3(l4.l3), + .l3_dout(l3_dout), + .l2a_dout(l2a_dout), .l1_2a_dout(l1_2a_dout), + .l0a_2a_dout(l0a_2a_dout), .l0b_2a_dout(l0b_2a_dout), + .l2b_dout(l2b_dout), .l1_2b_dout(l1_2b_dout), + .l0a_2b_dout(l0a_2b_dout), .l0b_2b_dout(l0b_2b_dout) + ); +endmodule + +module t; + logic clk = 0; + int cyc = 0; + + localparam type TOP_T = logic [3:0]; + localparam type L0A_T = logic [15:0]; + + l4_if #(.T(TOP_T), .L0A_T(L0A_T)) inst(); + + logic [3:0] l4_dout; + logic [7:0] l3_dout; + logic [15:0] l2a_dout; + logic [31:0] l1_2a_dout; + logic [15:0] l0a_2a_dout; + logic [7:0] l0b_2a_dout; + logic [7:0] l2b_dout; + logic [15:0] l1_2b_dout; + logic [15:0] l0a_2b_dout; + logic [7:0] l0b_2b_dout; + + l4_handler #(.T(TOP_T), .L0A_T(L0A_T)) m_l4 ( + .clk(clk), .l4(inst), + .l4_dout(l4_dout), + .l3_dout(l3_dout), + .l2a_dout(l2a_dout), .l1_2a_dout(l1_2a_dout), + .l0a_2a_dout(l0a_2a_dout), .l0b_2a_dout(l0b_2a_dout), + .l2b_dout(l2b_dout), .l1_2b_dout(l1_2b_dout), + .l0a_2b_dout(l0a_2b_dout), .l0b_2b_dout(l0b_2b_dout) + ); + + always #5 clk = ~clk; + + always_ff @(posedge clk) begin + inst.tb_in <= cyc[3:0]; + inst.l3.tb_in <= cyc[7:0] + 8'd1; + inst.l3.l2a.tb_in <= cyc[15:0] + 16'd2; + inst.l3.l2a.l1.tb_in <= cyc[31:0] + 32'd3; + inst.l3.l2a.l1.l0a.tb_in <= cyc[15:0] + 16'd4; + inst.l3.l2a.l1.l0b.tb_in <= cyc[7:0] + 8'd5; + inst.l3.l2b.tb_in <= cyc[7:0] + 8'd6; + inst.l3.l2b.l1.tb_in <= cyc[15:0] + 16'd7; + inst.l3.l2b.l1.l0a.tb_in <= cyc[15:0] + 16'd8; + inst.l3.l2b.l1.l0b.tb_in <= cyc[7:0] + 8'd9; + end + + logic [3:0] exp_l4; + logic [7:0] exp_l3; + logic [15:0] exp_l2a; + logic [31:0] exp_l1_2a; + logic [15:0] exp_l0a_2a; + logic [7:0] exp_l0b_2a; + logic [7:0] exp_l2b; + logic [15:0] exp_l1_2b; + logic [15:0] exp_l0a_2b; + logic [7:0] exp_l0b_2b; + + always_ff @(posedge clk) begin + exp_l4 <= inst.tb_in ^ 4'hF; + exp_l3 <= inst.l3.tb_in ^ 8'hFF; + exp_l2a <= inst.l3.l2a.tb_in ^ 16'hFFFF; + exp_l1_2a <= inst.l3.l2a.l1.tb_in ^ 32'hFFFFFFFF; + exp_l0a_2a <= inst.l3.l2a.l1.l0a.tb_in ^ 16'hFFFF; + exp_l0b_2a <= inst.l3.l2a.l1.l0b.tb_in ^ 8'hFF; + exp_l2b <= inst.l3.l2b.tb_in ^ 8'hFF; + exp_l1_2b <= inst.l3.l2b.l1.tb_in ^ 16'hFFFF; + exp_l0a_2b <= inst.l3.l2b.l1.l0a.tb_in ^ 16'hFFFF; + exp_l0b_2b <= inst.l3.l2b.l1.l0b.tb_in ^ 8'hFF; + end + + always @(posedge clk) begin + cyc <= cyc + 1; + + if (cyc > 3) begin + if (l4_dout !== exp_l4) begin + $display("FAIL cyc=%0d: l4_dout=%h expected %h", cyc, l4_dout, exp_l4); + $stop; + end + if (l3_dout !== exp_l3) begin + $display("FAIL cyc=%0d: l3_dout=%h expected %h", cyc, l3_dout, exp_l3); + $stop; + end + if (l2a_dout !== exp_l2a) begin + $display("FAIL cyc=%0d: l2a_dout=%h expected %h", cyc, l2a_dout, exp_l2a); + $stop; + end + if (l1_2a_dout !== exp_l1_2a) begin + $display("FAIL cyc=%0d: l1_2a_dout=%h expected %h", cyc, l1_2a_dout, exp_l1_2a); + $stop; + end + if (l0a_2a_dout !== exp_l0a_2a) begin + $display("FAIL cyc=%0d: l0a_2a_dout=%h expected %h", cyc, l0a_2a_dout, exp_l0a_2a); + $stop; + end + if (l0b_2a_dout !== exp_l0b_2a) begin + $display("FAIL cyc=%0d: l0b_2a_dout=%h expected %h", cyc, l0b_2a_dout, exp_l0b_2a); + $stop; + end + if (l2b_dout !== exp_l2b) begin + $display("FAIL cyc=%0d: l2b_dout=%h expected %h", cyc, l2b_dout, exp_l2b); + $stop; + end + if (l1_2b_dout !== exp_l1_2b) begin + $display("FAIL cyc=%0d: l1_2b_dout=%h expected %h", cyc, l1_2b_dout, exp_l1_2b); + $stop; + end + if (l0a_2b_dout !== exp_l0a_2b) begin + $display("FAIL cyc=%0d: l0a_2b_dout=%h expected %h", cyc, l0a_2b_dout, exp_l0a_2b); + $stop; + end + if (l0b_2b_dout !== exp_l0b_2b) begin + $display("FAIL cyc=%0d: l0b_2b_dout=%h expected %h", cyc, l0b_2b_dout, exp_l0b_2b); + $stop; + end + end + + if (cyc == 20) begin + $write("*-* All Finished *-*\n"); + $finish; + end + end +endmodule diff --git a/test_regress/t/t_interface_nested_port_type_noinl.py b/test_regress/t/t_interface_nested_port_type_noinl.py new file mode 100755 index 000000000..21954d583 --- /dev/null +++ b/test_regress/t/t_interface_nested_port_type_noinl.py @@ -0,0 +1,20 @@ +#!/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('simulator') +test.top_filename = "t/t_interface_nested_port_type.v" + +# Type parameters in nested interfaces (no-inline mode) +test.compile(verilator_flags2=['--binary', '-fno-inline']) + +test.execute() + +test.passes()