From 529d98918a020ce73631babc0607c3c187ea4d8f Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Tue, 15 Sep 2026 21:17:26 -0400 Subject: [PATCH] Fix deferring parameters for class::localparam access (#7746) (#7748) --- docs/CONTRIBUTORS | 2 + src/V3AstNodes.cpp | 4 +- src/V3EmitCSyms.cpp | 16 +- src/V3LinkDot.cpp | 83 ++++- src/V3Param.cpp | 318 ++++++++++++++---- .../t/t_class_lparam_circular_bad.out | 14 + test_regress/t/t_class_lparam_circular_bad.py | 16 + test_regress/t/t_class_lparam_circular_bad.v | 43 +++ test_regress/t/t_class_lparam_cycle_bad.out | 6 + test_regress/t/t_class_lparam_cycle_bad.py | 16 + test_regress/t/t_class_lparam_cycle_bad.v | 32 ++ test_regress/t/t_class_lparam_deferred.py | 18 + test_regress/t/t_class_lparam_deferred.v | 294 ++++++++++++++++ .../t/t_class_lparam_generate_inline.py | 18 + .../t/t_class_lparam_generate_inline.v | 87 +++++ .../t/t_class_lparam_nonparam_bad.out | 21 ++ test_regress/t/t_class_lparam_nonparam_bad.py | 16 + test_regress/t/t_class_lparam_nonparam_bad.v | 28 ++ test_regress/t/t_class_lparam_repeat_ref.py | 18 + test_regress/t/t_class_lparam_repeat_ref.v | 59 ++++ .../t/t_class_lparam_sibling_chain.py | 18 + test_regress/t/t_class_lparam_sibling_chain.v | 60 ++++ .../t_class_lparam_struct_field_chain_bad.out | 19 ++ .../t_class_lparam_struct_field_chain_bad.py | 16 + .../t/t_class_lparam_struct_field_chain_bad.v | 43 +++ test_regress/t/t_class_lparam_type_param.py | 18 + test_regress/t/t_class_lparam_type_param.v | 48 +++ test_regress/t/t_class_param_struct.py | 18 + test_regress/t/t_class_param_struct.v | 90 +++++ .../t/t_class_param_struct_collision.py | 19 ++ test_regress/t/t_class_scope_chain.py | 18 + test_regress/t/t_class_scope_chain.v | 120 +++++++ .../t/t_class_scope_chain_alias_bad.out | 5 + .../t/t_class_scope_chain_alias_bad.py | 16 + .../t/t_class_scope_chain_alias_bad.v | 48 +++ test_regress/t/t_class_scope_chain_bad.out | 14 + test_regress/t/t_class_scope_chain_bad.py | 16 + test_regress/t/t_class_scope_chain_bad.v | 37 ++ test_regress/t/t_class_scope_pkg_alias.py | 18 + test_regress/t/t_class_scope_pkg_alias.v | 79 +++++ test_regress/t/t_class_type_dep_param.py | 18 + test_regress/t/t_class_type_dep_param.v | 112 ++++++ test_regress/t/t_parse_sync_bad2.out | 13 - test_regress/t/t_struct_param_public.py | 21 ++ test_regress/t/t_struct_param_public.v | 38 +++ 45 files changed, 1940 insertions(+), 91 deletions(-) create mode 100644 test_regress/t/t_class_lparam_circular_bad.out create mode 100755 test_regress/t/t_class_lparam_circular_bad.py create mode 100644 test_regress/t/t_class_lparam_circular_bad.v create mode 100644 test_regress/t/t_class_lparam_cycle_bad.out create mode 100755 test_regress/t/t_class_lparam_cycle_bad.py create mode 100644 test_regress/t/t_class_lparam_cycle_bad.v create mode 100755 test_regress/t/t_class_lparam_deferred.py create mode 100644 test_regress/t/t_class_lparam_deferred.v create mode 100755 test_regress/t/t_class_lparam_generate_inline.py create mode 100644 test_regress/t/t_class_lparam_generate_inline.v create mode 100644 test_regress/t/t_class_lparam_nonparam_bad.out create mode 100755 test_regress/t/t_class_lparam_nonparam_bad.py create mode 100644 test_regress/t/t_class_lparam_nonparam_bad.v create mode 100755 test_regress/t/t_class_lparam_repeat_ref.py create mode 100644 test_regress/t/t_class_lparam_repeat_ref.v create mode 100755 test_regress/t/t_class_lparam_sibling_chain.py create mode 100644 test_regress/t/t_class_lparam_sibling_chain.v create mode 100644 test_regress/t/t_class_lparam_struct_field_chain_bad.out create mode 100755 test_regress/t/t_class_lparam_struct_field_chain_bad.py create mode 100644 test_regress/t/t_class_lparam_struct_field_chain_bad.v create mode 100755 test_regress/t/t_class_lparam_type_param.py create mode 100644 test_regress/t/t_class_lparam_type_param.v create mode 100755 test_regress/t/t_class_param_struct.py create mode 100644 test_regress/t/t_class_param_struct.v create mode 100755 test_regress/t/t_class_param_struct_collision.py create mode 100755 test_regress/t/t_class_scope_chain.py create mode 100644 test_regress/t/t_class_scope_chain.v create mode 100644 test_regress/t/t_class_scope_chain_alias_bad.out create mode 100755 test_regress/t/t_class_scope_chain_alias_bad.py create mode 100644 test_regress/t/t_class_scope_chain_alias_bad.v create mode 100644 test_regress/t/t_class_scope_chain_bad.out create mode 100755 test_regress/t/t_class_scope_chain_bad.py create mode 100644 test_regress/t/t_class_scope_chain_bad.v create mode 100755 test_regress/t/t_class_scope_pkg_alias.py create mode 100644 test_regress/t/t_class_scope_pkg_alias.v create mode 100755 test_regress/t/t_class_type_dep_param.py create mode 100644 test_regress/t/t_class_type_dep_param.v create mode 100755 test_regress/t/t_struct_param_public.py create mode 100644 test_regress/t/t_struct_param_public.v diff --git a/docs/CONTRIBUTORS b/docs/CONTRIBUTORS index ed287f13c..1f5d4d8f9 100644 --- a/docs/CONTRIBUTORS +++ b/docs/CONTRIBUTORS @@ -79,6 +79,7 @@ Drew Risinger Drew Taussig Driss Hafdi Edgar E. Iglesias +Edmund Lam Eddie Rydell em2machine emmettifelts @@ -330,6 +331,7 @@ Tymoteusz Blazejczyk Tyrone Marhguy Udaya Raj Subedi Udi Finkelstein +UnsignedByte Unai Martinez-Corral Valentin Atepalikhin Varun Koyyalagunta diff --git a/src/V3AstNodes.cpp b/src/V3AstNodes.cpp index 1a26409d7..6f1a78748 100644 --- a/src/V3AstNodes.cpp +++ b/src/V3AstNodes.cpp @@ -2374,7 +2374,9 @@ bool AstNodeDType::isLiteralType() const VL_MT_STABLE { if (const auto* const dtypep = VN_CAST(skipRefp(), BasicDType)) { return dtypep->keyword().isLiteralType(); } else if (const auto* const dtypep = VN_CAST(skipRefp(), UnpackArrayDType)) { - return dtypep->basicp()->isLiteralType(); + // basicp() is null for e.g. an array of unpacked structs, which is not literal + const AstBasicDType* const basicp = dtypep->basicp(); + return basicp && basicp->isLiteralType(); } else if (const auto* const dtypep = VN_CAST(skipRefp(), StructDType)) { // Currently all structs are packed, later this can be expanded to // 'forall members _.isLiteralType()' diff --git a/src/V3EmitCSyms.cpp b/src/V3EmitCSyms.cpp index 6490a911c..42272175d 100644 --- a/src/V3EmitCSyms.cpp +++ b/src/V3EmitCSyms.cpp @@ -421,13 +421,19 @@ class EmitCSyms final : EmitCBaseVisitorConst { stmt += protect("__Vscopep_" + svd.m_scopeName); stmt += needsEntSize ? "->varInsertSized(\"" : "->varInsert(\""; stmt += V3OutFormatter::quoteNameControls(prettyName) + '"'; - stmt += ", &("; - stmt += VIdProtect::protectIf(scopep->nameDotless(), scopep->protect()); - stmt += "."; - stmt += cName; - stmt += "), false, "; const std::string varName = VIdProtect::protectIf(scopep->nameDotless(), scopep->protect()) + "." + cName; + // A parameter is emitted 'static const', so its members are const too and + // need the same cast the whole-parameter insert uses. + if (svd.m_varp->isParam()) { + stmt += ", const_cast(static_cast(&("; + stmt += varName; + stmt += "))), true, "; + } else { + stmt += ", &("; + stmt += varName; + stmt += "), false, "; + } const std::string entSize = needsEntSize ? "sizeof(" + varName + ") / " + std::to_string(getUnpackedElements(dtypep)) diff --git a/src/V3LinkDot.cpp b/src/V3LinkDot.cpp index b2f452020..f6a159eb8 100644 --- a/src/V3LinkDot.cpp +++ b/src/V3LinkDot.cpp @@ -1030,9 +1030,11 @@ public: static bool checkIfClassOrPackage(const VSymEnt* const symp) { if (VN_IS(symp->nodep(), Class) || VN_IS(symp->nodep(), Package)) return true; - // Helper: check if a RefDType might resolve to a class later - const auto checkUnresolvedRef = [](const AstRefDType* refp) -> bool { - return refp && !refp->typeofp() && !refp->classOrPackageOpp(); + // Helper: check if a RefDType might resolve to a class later. + const auto isPotentialClassRef = [](const AstRefDType* refp) -> bool { + return refp && !refp->typeofp() + && (!refp->classOrPackageOpp() + || VN_IS(refp->classOrPackageOpp(), ClassOrPackageRef)); }; // Helper: allow types that can represent a class/package handle or an unresolved ref. @@ -1046,7 +1048,7 @@ public: const AstNodeDType* dtypep = typedefp->subDTypep(); if (!dtypep) dtypep = typedefp->childDTypep(); if (VN_IS(dtypep, ClassRefDType)) return true; - if (checkUnresolvedRef(VN_CAST(dtypep, RefDType))) return true; + if (isPotentialClassRef(VN_CAST(dtypep, RefDType))) return true; } else if (const AstParamTypeDType* const paramTypep = VN_CAST(symp->nodep(), ParamTypeDType)) { // Before V3Param the declared default is in childDTypep (possibly @@ -1058,8 +1060,9 @@ public: childp = reqp->lhsp(); } const AstNode* const checkp = childp ? childp : paramTypep->skipRefp(); - if (isValidTypeNode(checkp)) return true; - if (checkUnresolvedRef(VN_CAST(checkp, RefDType))) return true; + if (isValidTypeNode(checkp) || isPotentialClassRef(VN_CAST(checkp, RefDType))) { + return true; + } } return false; } @@ -4850,7 +4853,8 @@ class LinkDotResolveVisitor final : public VNVisitor { VL_RESTORER_COPY(m_ds); VL_RESTORER(m_pinSymp); - if (!nodep->classOrPackageSkipp() && nodep->name() != "local::") { + if (!nodep->classOrPackageSkipp() && !nodep->classOrPackageNodep() + && nodep->name() != "local::") { const bool deferIfUnresolved = m_statep->forPrimary() && m_insideClassExtParam; m_statep->resolveClassOrPackage(m_ds.m_dotSymp, nodep, m_ds.m_dotPos != DP_PACKAGE, false, ":: reference", deferIfUnresolved); @@ -5998,6 +6002,54 @@ class LinkDotResolveVisitor final : public VNVisitor { } m_ds.m_dotSymp = VL_RESTORER_PREV(m_curSymp); } + + // Collect the ClassOrPackageRefs in a chained scope operand in source order. + // The grammar builds a scope operand only from packageClassScopeItem, which + // always yields a ClassOrPackageRef, nested in Dots, so nothing else appears. + static void collectScopeRefs(AstNode* nodep, std::vector& refps) { + if (AstClassOrPackageRef* const refp = VN_CAST(nodep, ClassOrPackageRef)) { + refps.push_back(refp); + return; + } + AstDot* const dotp = VN_CAST(nodep, Dot); + UASSERT_OBJ(dotp, nodep, "Non-scope node in package/class scope operand"); + collectScopeRefs(dotp->lhsp(), refps); + collectScopeRefs(dotp->rhsp(), refps); + } + + // Resolve a chained class-scope operand (`pkg::outer::inner` in + // `pkg::outer::inner::t`) and reduce it to its innermost ClassOrPackageRef. + // Returns false, having reported, if a segment could not be resolved. + bool reduceScopeDot(AstRefDType* nodep, AstDot* scopeDotp) { + std::vector refps; + collectScopeRefs(scopeDotp, refps); + + VSymEnt* scopeSymp = m_ds.m_dotSymp; + for (size_t i = 0; i < refps.size(); ++i) { + AstClassOrPackageRef* const refp = refps[i]; + if (!refp->classOrPackageSkipp() && !refp->classOrPackageNodep() + && !m_statep->resolveClassOrPackage(scopeSymp, refp, i == 0, false, + "class/package reference")) { + return false; // Error already reported + } + if (i + 1 == refps.size()) break; + // A middle segment that resolved to a node with no reachable module + // (e.g. a typedef alias of a not-yet-specialized parameterized class) + // gives nothing to look the next segment up in. + AstNodeModule* const modp = refp->classOrPackageSkipp(); + if (!modp) { + refp->v3warn(E_UNSUPPORTED, "Unsupported: Multiple '::' package/class reference"); + return false; + } + scopeSymp = m_statep->getNodeSym(modp); + } + AstClassOrPackageRef* const innerp = refps.back(); + innerp->unlinkFrBack(); + VL_DO_DANGLING(pushDeletep(scopeDotp->unlinkFrBack()), scopeDotp); + nodep->classOrPackageOpp(innerp); + return true; + } + void visit(AstRefDType* nodep) override { if (auto* const typeOfp = nodep->typeofp()) { @@ -6064,7 +6116,16 @@ class LinkDotResolveVisitor final : public VNVisitor { } LINKDOT_VISIT_START(); UINFO(5, indent() << "visit " << nodep); - if (AstNode* const cpackagep = nodep->classOrPackageOpp()) { + if (nodep->classOrPackageOpp()) { + // Resolve a chained scope (`pkg::cls::t`, `pkg::cls#(P)::t`) from outside in. + // On failure reduceScopeDot has reported, so stop before using the operand. + if (AstDot* const scopeDotp = VN_CAST(nodep->classOrPackageOpp(), Dot)) { + if (!reduceScopeDot(nodep, scopeDotp)) return; + } + // Re-read: reduceScopeDot may have replaced the operand + AstNode* const cpackagep = nodep->classOrPackageOpp(); + UASSERT_OBJ(!VN_IS(cpackagep, Dot), cpackagep, + "Package/class scope operand should be reduced by now"); if (AstClassOrPackageRef* const cpackagerefp = VN_CAST(cpackagep, ClassOrPackageRef)) { iterate(cpackagerefp); const AstClass* const clsp = VN_CAST(cpackagerefp->classOrPackageNodep(), Class); @@ -6083,7 +6144,8 @@ class LinkDotResolveVisitor final : public VNVisitor { } const bool doDefaultTypedef = !(m_resolvingTypedef && m_statep->forPrimary()); - if (!cpackagerefp->classOrPackageSkipp(doDefaultTypedef)) { + if (!cpackagerefp->classOrPackageSkipp(doDefaultTypedef) + && !cpackagerefp->classOrPackageNodep()) { VSymEnt* const foundp = m_statep->resolveClassOrPackage( m_ds.m_dotSymp, cpackagerefp, true, false, "class/package reference"); if (!foundp) return; @@ -6105,9 +6167,6 @@ class LinkDotResolveVisitor final : public VNVisitor { << "'\n" << cpackagerefp->warnMore() + "... Suggest '.' instead of '::'"); } - } else { - cpackagep->v3warn(E_UNSUPPORTED, - "Unsupported: Multiple '::' package/class reference"); } VL_DO_DANGLING(pushDeletep(cpackagep->unlinkFrBack()), cpackagep); } diff --git a/src/V3Param.cpp b/src/V3Param.cpp index 8c6d65fab..ddd58e83b 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -341,6 +341,10 @@ class ParamProcessor final { return st; } + static bool isAggregateParamValue(const AstNode* nodep) { + return VN_IS(nodep, InitArray) || VN_IS(nodep, ConsPackUOrStruct); + } + static string paramValueString(const AstNode* nodep) { if (const AstRefDType* const refp = VN_CAST(nodep, RefDType)) { nodep = refp->skipRefToNonRefp(); @@ -387,6 +391,14 @@ class ParamProcessor final { key += ","; } key += "}"; + } else if (const AstConsPackUOrStruct* const structp = VN_CAST(nodep, ConsPackUOrStruct)) { + key += "{"; + for (const AstConsPackMember* memberp = structp->membersp(); memberp; + memberp = VN_AS(memberp->nextp(), ConsPackMember)) { + key += paramValueString(memberp->rhsp()); + key += ","; + } + key += "}"; } else if (const AstClassRefDType* const classRefp = VN_CAST(nodep, ClassRefDType)) { // For parameterized class types, use the original class name (without specialization // suffix) plus the actual type parameter values. This ensures equivalent class types @@ -1196,15 +1208,105 @@ class ParamProcessor final { } } + // Fold a param/lparam member's value to a Const in place. Resolves any + // class::member Dots in the value first (including those buried behind + // VarRefs to sibling lparams that are not yet const, e.g. `one = base` + // where `base = inner_a::v`), resolving siblings deepest-first so the + // whole chain constifies. `inProgress` guards against reference cycles. + void constifyMemberValue(AstVar* varp, std::set& inProgress) { + if (!varp->valuep() || VN_IS(varp->valuep(), Const)) return; + if (!inProgress.insert(varp).second) { + // Re-entered while still resolving varp: its value transitively + // references itself. Report and replace the value with a Const, as + // otherwise the unresolved Dot reaches V3Width as an untyped node. + varp->v3error("Variable's initial value is circular: " << varp->prettyNameQ()); + varp->valuep()->unlinkFrBack()->deleteTree(); + varp->valuep(new AstConst{varp->fileline(), AstConst::Signed32{}, 0}); + return; + } + // First resolve sibling lparams this value references, so their Dots + // are folded before we inline them here. + std::set siblings; + varp->valuep()->foreach([&](const AstVarRef* refp) { + AstVar* const refVarp = refp->varp(); + if (refVarp && refVarp->isParam() && refVarp->valuep() + && !VN_IS(refVarp->valuep(), Const)) { + siblings.insert(refVarp); + } + }); + for (AstVar* const sibp : siblings) constifyMemberValue(sibp, inProgress); + // Resolve class-scoped references and any member selections they expose. + resolveDeferredDotsReachableFrom(varp->valuep(), m_modp); + V3Const::constifyParamsEdit(varp); + inProgress.erase(varp); + } + + // Return the member name on a deferred Dot RHS, including a selected member. + static AstParseRef* memberParseRef(AstNode* nodep) { + if (AstParseRef* const refp = VN_CAST(nodep, ParseRef)) return refp; + AstNodePreSel* const selp = VN_CAST(nodep, NodePreSel); + return selp ? VN_CAST(selp->fromp(), ParseRef) : nullptr; + } + + // Replace a deferred Dot with valuep, preserving any select on the member name. + static void replaceMemberDot(AstDot* dotp, AstParseRef* memberRefp, AstNodeExpr* valuep) { + if (memberRefp == dotp->rhsp()) { + dotp->replaceWith(valuep); + } else { + AstNodePreSel* const selp = VN_AS(dotp->rhsp(), NodePreSel); + if (AstAttrOf* const attrp = selp->attrp()) { + if (AstNode* const oldFromp = attrp->fromp()) { + oldFromp->replaceWith(valuep->cloneTree(false)); + VL_DO_DANGLING(oldFromp->deleteTree(), oldFromp); + } + } + memberRefp->replaceWith(valuep); + VL_DO_DANGLING(memberRefp->deleteTree(), memberRefp); + dotp->replaceWith(dotp->rhsp()->unlinkFrBack()); + } + VL_DO_DANGLING(dotp->deleteTree(), dotp); + } + + // True for an expression produced while lowering a deferred struct-member chain. + static bool isDeferredMemberBase(const AstNodeExpr* nodep) { + if (VN_IS(nodep, MemberSel)) return true; + if (const AstNodePreSel* const selp = VN_CAST(nodep, NodePreSel)) { + const AstNodeDType* const fromDTypep = selp->fromp()->dtypep(); + const AstNodeArrayDType* const arrayDTypep + = fromDTypep ? VN_CAST(fromDTypep->skipRefOrNullp(), NodeArrayDType) : nullptr; + if (arrayDTypep) { + const AstNodeDType* const elemDTypep = arrayDTypep->subDTypep(); + return elemDTypep && VN_IS(elemDTypep->skipRefOrNullp(), NodeUOrStructDType); + } + return isDeferredMemberBase(selp->fromp()); + } + const AstNodeDType* const dtypep = nodep->dtypep(); + return dtypep && VN_IS(dtypep->skipRefOrNullp(), NodeUOrStructDType); + } + + // Lower a Dot exposed after its inner class::member reference was substituted. + static void resolveMemberDot(AstDot* dotp) { + AstNodeExpr* const lhsp = VN_CAST(dotp->lhsp(), NodeExpr); + AstParseRef* const memberRefp = memberParseRef(dotp->rhsp()); + if (!lhsp || !memberRefp || !isDeferredMemberBase(lhsp)) return; + AstMemberSel* const newp = new AstMemberSel{memberRefp->fileline(), lhsp->unlinkFrBack(), + VFlagChildDType{}, memberRefp->name()}; + replaceMemberDot(dotp, memberRefp, newp); + } + // Helper to resolve DOT to RefDType for class type references. // If the class is parameterized and not yet specialized, specialize it first. // This handles cases like: iface #(param_class#(value)::typedef_name) void resolveDotToTypedef(AstNode* exprp) { AstDot* const dotp = VN_CAST(exprp, Dot); if (!dotp) return; + UINFO(9, "Resolve deferred Dot: " << dotp); AstClassOrPackageRef* const classRefp = VN_CAST(dotp->lhsp(), ClassOrPackageRef); - if (!classRefp) return; - AstParseRef* const parseRefp = VN_CAST(dotp->rhsp(), ParseRef); + if (!classRefp) { + resolveMemberDot(dotp); + return; + } + AstParseRef* const parseRefp = memberParseRef(dotp->rhsp()); if (!parseRefp) return; const AstClass* lhsClassp = VN_CAST(classRefp->classOrPackageSkipp(), Class); @@ -1248,37 +1350,37 @@ class ParamProcessor final { if (!lhsClassp) return; AstNode* const memberp = m_memberMap.findMember(lhsClassp, parseRefp->name()); + UINFO(9, "Resolve deferred class member: " << parseRefp->name() << " -> " << memberp); if (AstTypedef* const tdefp = VN_CAST(memberp, Typedef)) { + if (parseRefp != dotp->rhsp()) return; AstRefDType* const refp = new AstRefDType{dotp->fileline(), tdefp->name()}; refp->typedefp(tdefp); dotp->replaceWith(refp); VL_DO_DANGLING(dotp->deleteTree(), dotp); } else if (AstVar* const varp = VN_CAST(memberp, Var)) { - // Param/lparam member: substitute its constant value so the caller's constify can - // succeed. - if (varp->isParam() && varp->valuep()) { - if (!VN_IS(varp->valuep(), Const)) V3Const::constifyParamsEdit(varp); - if (AstConst* const constp = VN_CAST(varp->valuep(), Const)) { - dotp->replaceWith(constp->cloneTree(false)); - VL_DO_DANGLING(dotp->deleteTree(), dotp); - } - } + substituteParamMember(dotp, parseRefp, varp); } } - // Resolve an unresolved RefDType whose classOrPackageOp targets a parameterized - // class or a typedef alias of one. Specializes the class and links the typedef. - void resolveParamClassRefDType(AstNodeDType* dtypep) { - // Recurse into struct/union members for buried RefDTypes - if (AstNodeUOrStructDType* const sup = VN_CAST(dtypep, NodeUOrStructDType)) { - for (AstMemberDType* memp = sup->membersp(); memp; - memp = VN_AS(memp->nextp(), MemberDType)) { - resolveParamClassRefDType(memp->subDTypep()); - } - return; + // Substitute a class param/lparam member's constant value for dotp. The + // reverse-order deferred-Dot walk lowers any outer fields to MemberSels. + void substituteParamMember(AstDot* const dotp, AstParseRef* const memberRefp, + AstVar* const varp) { + if (!varp->isParam() || !varp->valuep()) return; + if (!VN_IS(varp->valuep(), Const)) { + std::set inProgress; + constifyMemberValue(varp, inProgress); } - AstRefDType* const refp = dtypep ? VN_CAST(dtypep, RefDType) : nullptr; - if (!refp) return; + AstConst* const constp = VN_CAST(varp->valuep(), Const); + if (!constp) return; + AstConst* const newp = constp->cloneTree(false); + newp->dtypep(varp->subDTypep()); + replaceMemberDot(dotp, memberRefp, newp); + } + + // Resolve one class-scoped RefDType. The generic deferred walk finds these + // through every dtype wrapper and follows out-of-tree typedef references. + void resolveParamClassRefDType(AstRefDType* refp) { if (refp->typedefp() || refp->refDTypep()) return; AstClassOrPackageRef* const classRefp @@ -1422,30 +1524,32 @@ class ParamProcessor final { } } + // Include the pin's value in the specialization name, so cells passing equal + // values share one module clone. + void nameByPinValue(AstPin* pinp, AstNodeModule* srcModp, AstVar* modvarp, string& longnamer, + bool& any_overridesr) { + longnamer += "_" + paramSmallName(srcModp, modvarp) + paramValueNumber(pinp->exprp()); + any_overridesr = true; + } + void cellPinCleanup(AstNode* nodep, AstPin* pinp, AstPin* paramsp, AstNodeModule* srcModp, string& longnamer, bool& any_overridesr) { if (!pinp->exprp()) return; // No-connect if (AstVar* const modvarp = pinp->modVarp()) { - resolveParamClassRefDType(modvarp->subDTypep()); + resolveDeferredDotsReachableFrom(modvarp->subDTypep(), m_modp); if (!modvarp->isGParam()) { pinp->v3fatalSrc("Attempted parameter setting of non-parameter: Param " << pinp->prettyNameQ() << " of " << nodep->prettyNameQ()); } else if (VN_IS(pinp->exprp(), InitArray) && arraySubDTypep(modvarp->subDTypep())) { // Array assigned to array - AstNode* const exprp = pinp->exprp(); - longnamer += "_" + paramSmallName(srcModp, modvarp) + paramValueNumber(exprp); - any_overridesr = true; - } else if (VN_IS(pinp->exprp(), InitArray)) { - // Array assigned to scalar parameter. Treat the InitArray as a constant - // integer array and include it in the module name. Constantify nested - // expressions before mangling the value number. - V3Const::constifyParamsEdit(pinp->exprp()); - longnamer - += "_" + paramSmallName(srcModp, modvarp) + paramValueNumber(pinp->exprp()); - any_overridesr = true; + nameByPinValue(pinp, srcModp, modvarp, longnamer, any_overridesr); } else { UINFO(9, "cellPinCleanup: before constify " << pinp << " " << modvarp); V3Const::constifyParamsEdit(pinp->exprp()); + if (isAggregateParamValue(pinp->exprp())) { + nameByPinValue(pinp, srcModp, modvarp, longnamer, any_overridesr); + return; + } // Cast/CastSize default values are not yet folded by V3Width. // Constify here so the comparison below sees a Const node. // Other node kinds are handled in the branches above. @@ -1606,15 +1710,10 @@ class ParamProcessor final { if (normedNamep) VL_DO_DANGLING(normedNamep->deleteTree(), normedNamep); } } else if (AstParamTypeDType* const modvarp = pinp->modPTypep()) { - // Handle DOT with ParseRef RHS (e.g., p_class#(8)::p_type) - // by this point ClassOrPackageRef should be updated to point to the specialized class. - resolveDotToTypedef(pinp->exprp()); - resolveParamClassRefDType(modvarp->subDTypep()); - // Also resolve through the pin expression's typedef chain - if (const AstRefDType* const pinRefp = VN_CAST(pinp->exprp(), RefDType)) { - if (const AstTypedef* const tdefp = pinRefp->typedefp()) - resolveParamClassRefDType(tdefp->subDTypep()); - } + // Resolve the pin and declared type through arbitrary dtype wrappers + // and typedef chains before widthing either one. + resolveDeferredDotsReachableFrom(pinp->exprp(), m_modp); + resolveDeferredDotsReachableFrom(modvarp->subDTypep(), m_modp); AstNodeDType* rawTypep = VN_CAST(pinp->exprp(), NodeDType); // Guard against widthing a struct/union still owned by a @@ -2155,6 +2254,67 @@ class ParamProcessor final { return newClassp; } + // Resolve deferred class-scoped references in post-order. Resolution may + // deparameterize a class and delete its parameter pins, so no pointer to a + // child may remain pending when its parent is resolved. + class DeferredResolverVisitor final : public VNVisitor { + ParamProcessor& m_processor; + std::set m_reachedDecls; + + bool firstReach(const AstNode* const declp) { return m_reachedDecls.insert(declp).second; } + + void iterateParamType(AstParamTypeDType* const paramTypep) { + if (firstReach(paramTypep)) iterate(paramTypep); + } + void iterateTypedef(AstTypedef* const tdefp) { + if (firstReach(tdefp)) iterate(tdefp->subDTypep()); + } + + void visit(AstNode* nodep) override { iterateChildren(nodep); } + void visit(AstClassOrPackageRef* nodep) override { + iterateChildren(nodep); + AstNode* const targetp = nodep->classOrPackageNodep(); + if (AstTypedef* const tdefp = VN_CAST(targetp, Typedef)) { + iterateTypedef(tdefp); + } else if (AstParamTypeDType* const paramTypep = VN_CAST(targetp, ParamTypeDType)) { + iterateParamType(paramTypep); + } + } + void visit(AstDot* nodep) override { + iterateChildren(nodep); + m_processor.resolveDotToTypedef(nodep); + } + void visit(AstRefDType* nodep) override { + iterateChildren(nodep); + AstTypedef* const tdefp = nodep->typedefp(); + if (tdefp) { + iterateTypedef(tdefp); + } else if (AstParamTypeDType* const paramTypep + = VN_CAST(nodep->refDTypep(), ParamTypeDType)) { + iterateParamType(paramTypep); + } + m_processor.resolveParamClassRefDType(nodep); + } + void visit(AstVarRef* nodep) override { + iterateChildren(nodep); + AstVar* const varp = nodep->varp(); + if (firstReach(varp)) { + iterate(varp->subDTypep()); + const auto& deferredVarps = v3Global.rootp()->deferredParamVarps(); + if (varp->varType() == VVarType::LPARAM && deferredVarps.count(varp)) { + UASSERT_OBJ(varp->valuep(), varp, "VarRef should have non-null valuep"); + iterate(varp->valuep()); + } + } + } + + public: + DeferredResolverVisitor(ParamProcessor& processor, AstNode* rootp) + : m_processor{processor} { + iterate(rootp); + } + }; + public: // After an interface cell inside parentModp has been deparameterized // (rewired from template to clone), retarget REFDTYPEs that still @@ -2181,6 +2341,16 @@ public: }); } + // Resolve deferred class-scoped values and types reachable from rootp. In + // addition to AST children, follow typedefs, parameter types, deferred + // localparam values, and referenced-variable dtypes. This makes the walk + // independent of the particular dtype wrappers around a RefDType. + void resolveDeferredDotsReachableFrom(AstNode* rootp, const AstNodeModule* modp) { + VL_RESTORER(m_modp); + m_modp = modp; + DeferredResolverVisitor{*this, rootp}; + } + AstNodeModule* nodeDeparam(AstNode* nodep, AstNodeModule* srcModp, AstNodeModule* modp, const string& someInstanceName) { // Return new or reused de-parameterized module @@ -2221,14 +2391,9 @@ public: } // Create new module name with _'s between the constants UINFOTREE(10, nodep, "", "cell"); - // Resolve `class::member` Dots in pin values so constify sees Consts. - // Single-pass: filter-collect class-scoped Dots, then reverse-iterate so inner - // Dots resolve before outer (vector stays empty for cells with no class Dots). - std::vector dotps; - nodep->foreach([&](AstDot* dotp) { - if (VN_IS(dotp->lhsp(), ClassOrPackageRef)) dotps.push_back(dotp); - }); - for (auto it = dotps.rbegin(); it != dotps.rend(); ++it) resolveDotToTypedef(*it); + // Resolve `class::member` Dots in pin values, and in any deferred + // lparam reachable from the pin tree, so constify sees Consts. + resolveDeferredDotsReachableFrom(nodep, modp); // Evaluate all module constants V3Const::constifyParamsEdit(nodep); // Set name for warnings for when we param propagate the module @@ -2583,6 +2748,7 @@ class ParamVisitor final : public VNVisitor { VarsByName m_modIfaceRefs; // Interface-ref Vars in current module, keyed by name bool m_modIfaceRefsDone = false; // m_modIfaceRefs has been gathered for m_modp string m_generateHierName; // Generate portion of hierarchy name + bool m_inGenerateCond = false; // Traversing a generate condition; see iterateGenerateCond // METHODS @@ -2895,7 +3061,9 @@ class ParamVisitor final : public VNVisitor { // Visit parameters in the instantiation. iterateChildren(nodep); - m_cellps.emplace(!isIface, nodep); + // A generate condition is folded and deleted before the drain loop runs, so + // queueing from one only leaves a dangling pointer. See iterateGenerateCond. + if (!m_inGenerateCond) m_cellps.emplace(!isIface, nodep); } // VISITORS @@ -2949,7 +3117,14 @@ class ParamVisitor final : public VNVisitor { } void visit(AstRefDType* nodep) override { - if (isCircularType(nodep)) { + const bool isCircular = isCircularType(nodep); + // A module-body `typedef C#(Cfg)::t alias` the deferred pin/param walk + // never reached would survive to V3Width unlinked. If its alias chain is + // broken, run the same deferred resolution here. + if (!isCircular && nodep->typedefp() && !nodep->skipRefOrNullp()) { + m_processor.resolveDeferredDotsReachableFrom(nodep, m_modp); + } + if (isCircular) { nodep->v3error("Typedef's type is circular: " << nodep->prettyName()); } else if (nodep->typedefp() && nodep->subDTypep() && (VN_IS(nodep->subDTypep()->skipRefOrNullp(), IfaceRefDType) @@ -3100,6 +3275,7 @@ class ParamVisitor final : public VNVisitor { v3Global.rootp()->pushDeferredParamVarp(nodep); return; } + m_processor.resolveDeferredDotsReachableFrom(nodep, m_modp); V3Const::constifyParamsEdit(nodep); } } @@ -3311,9 +3487,24 @@ class ParamVisitor final : public VNVisitor { } // Generate Statements + + // Traverse a generate condition (if/case expression, for init/cond/inc). + // + // m_cellps assumes a queued ref outlives the drain loop, which holds elsewhere + // because deletion is deferred. A generate condition doesn't defer and folds + // immediately, so we can't queue. Anything queued here would be left dangling. + // Refs in the surviving arm are queued when it is recursed. + void iterateGenerateCond(AstNode* nodep) { + VL_RESTORER(m_inGenerateCond); + m_inGenerateCond = true; + iterateAndNextNull(nodep); + // The condition may reference deferred lparams whose Dots are still pending. + m_processor.resolveDeferredDotsReachableFrom(nodep, m_modp); + } + void visit(AstGenIf* nodep) override { UINFO(9, " GENIF " << nodep); - iterateAndNextNull(nodep->condp()); + iterateGenerateCond(nodep->condp()); // We suppress errors when widthing params since short-circuiting in // the conditional evaluation may mean these error can never occur. We // then make sure that short-circuiting is used by constifyParamsEdit. @@ -3347,9 +3538,9 @@ class ParamVisitor final : public VNVisitor { UINFO(9, " BEGIN " << nodep); UINFO(9, " GENFOR " << forp); // Visit child nodes before unrolling - iterateAndNextNull(forp->initsp()); - iterateAndNextNull(forp->condp()); - iterateAndNextNull(forp->incsp()); + iterateGenerateCond(forp->initsp()); + iterateGenerateCond(forp->condp()); + iterateGenerateCond(forp->incsp()); V3Width::widthParamsEdit(forp); // Param typed widthing will NOT recurse the body // Outer wrapper around generate used to hold genvar, and to ensure genvar // doesn't conflict in V3LinkDot resolution with other genvars @@ -3381,8 +3572,15 @@ class ParamVisitor final : public VNVisitor { UINFO(9, " GENCASE " << nodep); bool hit = false; AstNode* keepp = nullptr; - iterateAndNextNull(nodep->exprp()); + iterateGenerateCond(nodep->exprp()); V3Case::caseLint(nodep); + // Case-item expressions must also be linked before widthing the case. + for (AstGenCaseItem* itemp = nodep->itemsp(); itemp; + itemp = VN_AS(itemp->nextp(), GenCaseItem)) { + for (AstNode* ep = itemp->condsp(); ep; ep = ep->nextp()) { + m_processor.resolveDeferredDotsReachableFrom(ep, m_modp); + } + } V3Width::widthParamsEdit(nodep); // Param typed widthing will NOT recurse the // body, don't trigger errors yet. V3Const::constifyParamsEdit(nodep->exprp()); // exprp may change @@ -3392,7 +3590,7 @@ class ParamVisitor final : public VNVisitor { itemp = VN_AS(itemp->nextp(), GenCaseItem)) { for (AstNode* ep = itemp->condsp(); ep;) { AstNode* const nextp = ep->nextp(); // May edit list - iterateAndNextNull(ep); + iterateGenerateCond(ep); VL_DO_DANGLING(V3Const::constifyParamsEdit(ep), ep); // ep may change ep = nextp; } diff --git a/test_regress/t/t_class_lparam_circular_bad.out b/test_regress/t/t_class_lparam_circular_bad.out new file mode 100644 index 000000000..cf135b8fe --- /dev/null +++ b/test_regress/t/t_class_lparam_circular_bad.out @@ -0,0 +1,14 @@ +%Error: t/t_class_lparam_circular_bad.v:16:18: Variable's initial value is circular: 'a' + : ... note: In instance 't' + 16 | localparam int a = a; + | ^ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Error: t/t_class_lparam_circular_bad.v:21:18: Variable's initial value is circular: 'a' + : ... note: In instance 't' + 21 | localparam int a = b; + | ^ +%Error: t/t_class_lparam_circular_bad.v:27:18: Variable's initial value is circular: 'p' + : ... note: In instance 't' + 27 | localparam int p = q; + | ^ +%Error: Exiting due to diff --git a/test_regress/t/t_class_lparam_circular_bad.py b/test_regress/t/t_class_lparam_circular_bad.py new file mode 100755 index 000000000..38cf36b43 --- /dev/null +++ b/test_regress/t/t_class_lparam_circular_bad.py @@ -0,0 +1,16 @@ +#!/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.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_class_lparam_circular_bad.v b/test_regress/t/t_class_lparam_circular_bad.v new file mode 100644 index 000000000..919d3db71 --- /dev/null +++ b/test_regress/t/t_class_lparam_circular_bad.v @@ -0,0 +1,43 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Negative test for the reference-cycle guard in V3Param's +// constifyMemberValue. A class localparam whose value transitively +// references itself is reached through a `class::member` Dot in a cell +// parameter pin, so the fold recurses back into the same AstVar while it is +// still being resolved. That must produce a clean user-facing error and +// leave a Const behind, NOT recurse forever or crash the following width +// pass on the still-unresolved Dot. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +class Self #(parameter int W = 1); + localparam int a = a; +endclass + +class Direct #(parameter int W = 1); + // Two-step cycle: a -> b -> a + localparam int a = b; + localparam int b = a; +endclass + +class Indirect #(parameter int W = 1); + // Three-step cycle through sibling lparams: p -> q -> r -> p + localparam int p = q; + localparam int q = r; + localparam int r = p; +endclass + +module Sub #(parameter int P = 0) (); +endmodule + +module t; + typedef Self#(4) SCFG; + typedef Direct#(4) DCFG; + typedef Indirect#(4) ICFG; + + Sub #(SCFG::a) u_self (); + Sub #(DCFG::a) u_direct (); + Sub #(ICFG::p) u_indirect (); +endmodule diff --git a/test_regress/t/t_class_lparam_cycle_bad.out b/test_regress/t/t_class_lparam_cycle_bad.out new file mode 100644 index 000000000..9caae73c0 --- /dev/null +++ b/test_regress/t/t_class_lparam_cycle_bad.out @@ -0,0 +1,6 @@ +%Error: t/t_class_lparam_cycle_bad.v:25:20: Variable's initial value is circular: 'a' + : ... note: In instance 't' + 25 | localparam int a = inner_a::v + b; + | ^ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Error: Exiting due to diff --git a/test_regress/t/t_class_lparam_cycle_bad.py b/test_regress/t/t_class_lparam_cycle_bad.py new file mode 100755 index 000000000..38cf36b43 --- /dev/null +++ b/test_regress/t/t_class_lparam_cycle_bad.py @@ -0,0 +1,16 @@ +#!/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.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_class_lparam_cycle_bad.v b/test_regress/t/t_class_lparam_cycle_bad.v new file mode 100644 index 000000000..c3469b215 --- /dev/null +++ b/test_regress/t/t_class_lparam_cycle_bad.v @@ -0,0 +1,32 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// A class localparam whose value is deferred (contains a class::member Dot) +// and also transitively references itself. V3Param's deferred-lparam fold +// must break the reference cycle and report it cleanly, rather than +// recursing forever or leaving an unresolved Dot that reaches V3Width as an +// untyped node ("Node has no type" internal error). +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module Sub #(parameter int WIDTH = 0) (); +endmodule + +module t; + virtual class A #(parameter int x = 0); + localparam int v = x * 2; + endclass + + virtual class B #(parameter int y = 0); + typedef A#(y + 1) inner_a; + // 'a' is deferred (holds the inner_a::v Dot) and also references 'b'; + // 'b' references 'a' back -> cycle through the deferred fold. + localparam int a = inner_a::v + b; + localparam int b = a; + endclass + + typedef B#(5) BInst; + + Sub #(BInst::a) m (); +endmodule diff --git a/test_regress/t/t_class_lparam_deferred.py b/test_regress/t/t_class_lparam_deferred.py new file mode 100755 index 000000000..46d1fe4c0 --- /dev/null +++ b/test_regress/t/t_class_lparam_deferred.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_class_lparam_deferred.v b/test_regress/t/t_class_lparam_deferred.v new file mode 100644 index 000000000..912c6d44a --- /dev/null +++ b/test_regress/t/t_class_lparam_deferred.v @@ -0,0 +1,294 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Consumers of a class-scope-resolved localparam (typedef alias of a +// parameterized class, e.g. inst::b). V3LinkDot defers the inst::b Dot until +// post-V3Param, so V3Param must resolve the deferred Dot everywhere such a +// value can be consumed - following VarRef chains into deferred lparams, and +// descending through typedefs and ParamTypeDTypes to reach buried Dots: +// (1) module and interface instance parameter pins +// (2) generate-for / -if / -case conditions +// (3) typedef ranges, class type args, and $bits of class-scope typedefs +// (4) nested class hierarchies (B2::width = inner_a::v) +// (5) struct localparam field access (CFG::cfg.jt.cam_type) +// (6) struct members typed by a class-scope typedef (CFG::data_t) +// (7) `localparam type` bound to a child module's type parameter +// +// Plain localparam value chains are covered by t_class_lparam_chain. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkh(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0) +// verilog_format: on + +typedef struct packed { + logic [7:0] cam_type; + logic [7:0] depth; +} inner_t; + +typedef struct packed { + inner_t jt; + logic [7:0] tag; +} cfg_t; + +typedef struct packed { + inner_t [1:0] entries; +} table_t; + +package pkg; + virtual class C #(parameter int W = 1); + typedef logic [W-1:0] data_t; + endclass +endpackage + +// Struct localparam whose fields derive from the class parameter, plus a +// wrapper class whose own lparam reads a nested struct field. +class SC #(parameter int W = 1); + localparam cfg_t cfg = '{jt: '{cam_type: W[7:0], depth: W[7:0] + 8'd1}, + tag: W[7:0] + 8'd2}; + localparam logic [7:0] bits = W[7:0]; + localparam inner_t [1:0] entries = '{default: '{cam_type: W[7:0], + depth: W[7:0] + 8'd1}}; + localparam table_t tbl = '{entries: '{default: '{cam_type: W[7:0], + depth: W[7:0] + 8'd1}}}; +endclass + +class SD #(parameter int W = 1); + typedef SC#(W) CC; + localparam int q = int'(CC::cfg.tag) + 100; +endclass + +class P #(parameter cfg_t p = '{default: 0}); + localparam cfg_t pp = p; +endclass + +module Sub #(parameter int WIDTH = 0) (); +endmodule + +// Two-level: outer module forwards its param to an inner cell, so the +// deferred lparam value must flow through the cell-deparam chain. +module SubL1 #(parameter int W = 0) (); + Sub #(W) inner (); +endmodule + +interface SubIface #(parameter int IW = 1) (); + logic [IW-1:0] data; +endinterface + +// Takes an interface port - must elaborate after the iface cell pin is +// fully constified. +module Consumer (SubIface si); +endmodule + +// Presence is observable, to confirm each generate flavour elaborated. +module Tag #(parameter int ID = 0) (); +endmodule + +module Sink #(parameter int BITS = 1) (); + logic [BITS-1:0] data; +endmodule + +// Parameterized wrapper so the class specialization is deferred until +// V3Param processes the cell instance. +module Mid #(parameter int W = 8) (); + typedef SC#(W) CFG; + // (5) single-level struct field access, then nested (lsb accumulation) + Sub #(int'(CFG::cfg.tag)) u_tag (); + Sub #(int'(CFG::cfg.jt.cam_type)) u_cam (); + Sub #(int'(CFG::cfg.jt.depth)) u_depth (); + Sub #(int'(CFG::bits[0])) u_bit (); + Sub #(int'(CFG::bits[3:1])) u_part (); + Sub #(int'(CFG::cfg.jt.cam_type[3])) u_field_bit (); + Sub #(int'(CFG::cfg.jt.cam_type[3:1])) u_field_part (); + Sub #(int'(CFG::entries[1].cam_type[3:1])) u_array_field_part (); + Sub #(int'(CFG::tbl.entries[1].cam_type[3:1])) u_member_array_field_part (); + // nested struct-field Dot buried in a wrapper class's lparam value + typedef SD#(W) DD; + Sub #(int'(DD::q)) u_q (); + + // (6) struct member typed by a class-scope typedef from a parameterized + // class, consumed by $bits on a cell pin. Without following typedefs the + // member RefDType stays unlinked and $bits trips an internal error. + typedef pkg::C#(W) PCFG; + typedef struct packed { + PCFG::data_t payload; + logic v; + } wrap_t; + Sink #(.BITS($bits(wrap_t))) u_sink (); +endmodule + +module PairHolder #(parameter cfg_t cfg = '{default: 0}) (); + typedef P#(cfg) PALIAS; + localparam logic [7:0] tag_val = PALIAS::pp.tag; + localparam logic [7:0] cam_val = PALIAS::pp.jt.cam_type; +endmodule + +// (7) The pin's RefDType has no typedefp() to follow - it points at the +// enclosing ParamTypeDType - so the deferred walk must descend refDTypep(). +module TChild #(parameter type T = logic, parameter int EXP = 1) (input T a_i); + initial `checkh($bits(T), EXP); +endmodule + +module TFwd #(parameter type T = logic, parameter int EXP = 1) (); + TChild #(.T(T), .EXP(EXP)) u (.a_i('0)); +endmodule + +module t; + virtual class C #(parameter int a = 0); + localparam int b = a; + typedef logic [a-1:0] inner_t; + // localparam derived from a typedef inside the same class + localparam int width = $bits(inner_t); + endclass + + // Two-level: B2's lparam value contains a class::member Dot of its own + virtual class A2 #(parameter int x = 0); + localparam int v = x * 2; + endclass + virtual class B2 #(parameter int y = 0); + typedef A2#(y + 1) inner_a; + localparam int width = inner_a::v; // nested Dot inside B2's lparam + // `sib` is deferred (holds the Dot); `sib_use` reads it as a sibling, so + // the sibling must be folded before this member's value can constify. + localparam int sib = inner_a::v; + localparam int sib_use = sib + 1; + endclass + // Three-level: C2 wraps B2 wraps A2 + virtual class C2 #(parameter int z = 0); + typedef B2#(z * 3) inner_b; + localparam int total = inner_b::width; // double-nested Dot + endclass + + typedef C#(3) c3; + typedef C#(4) c4; + typedef C#(5) c5; + typedef C#(8) c8; + typedef C#(13) c13; + typedef B2#(5) BInst; + typedef C2#(2) CInst; + + // Deferred lparams (value contains a class::member Dot) + localparam int b3 = c3::b; + localparam int b4 = c4::b; + localparam int b5 = c5::b; + localparam int b8 = c8::b; + localparam int b13 = c13::b; + // Chained: value references another deferred lparam + localparam int c8_ref = b8; + localparam int d8_ref = c8_ref + 1; + + // (4) nested class hierarchies + localparam int two_level = BInst::width; // = A2#(6)::v = 12 + localparam int three_level = CInst::total; // = B2#(6)::width = A2#(7)::v = 14 + localparam int nested_chain = BInst::width; + + // (1) module pins: bare VarRef, multi-lparam chain, expressions, a Dot + // mixed with an lparam, two-level forwarding, and nested-class Dots. + Sub #(b8) m_bare (); + Sub #(d8_ref) m_chain (); + Sub #(b4 + b5) m_expr (); + Sub #(c4::b + b5) m_mix (); + SubL1 #(b8) m_l1 (); + Sub #(BInst::width) m_pin_direct (); + Sub #(CInst::total) m_pin_deep (); + Sub #(BInst::width + 1) m_pin_expr (); + Sub #(nested_chain) m_pin_chain (); + // sibling-lparam fold: BInst::sib_use = A2#(6)::v + 1 = 13 + Sub #(BInst::sib_use) m_pin_sibling (); + + // (1) interface pins, plus an iface bound to a module port + SubIface #(b8) i_bare (); + SubIface #(c8_ref) i_chain (); + SubIface #(b8 + b13) i_expr (); + Consumer cons (.si(i_bare)); + + // (2) generate-for / -if / -case driven by deferred lparams + for (genvar i = 0; i < b3; i++) begin : gf + Tag #(100 + i) inst (); + end + if (b5 > b3) begin : gi_t + Tag #(200) inst (); + end else begin : gi_f + Tag #(201) inst (); + end + case (b5) + 3: begin : gc Tag #(303) inst (); end + 5: begin : gc Tag #(305) inst (); end + default: begin : gc Tag #(399) inst (); end + endcase + + // (3) typedef range from a deferred lparam; class type-arg using a + // deferred lparam; class-scope typedef via $bits; and a typedef whose + // packed range is built from class-scope Dots directly (no lparam). + typedef logic [b8-1:0] data_t; + data_t data_value; + typedef C#(b8) c_from_def; + localparam int from_def_b = c_from_def::b; + localparam int via_bits = c8::width; + logic [b8-1:0] wide_bus; + typedef logic [(c8::b + c8::b - 1):0] direct_use_t; + Sub #(.WIDTH($bits(direct_use_t))) u_sub (); + + // (5)/(6) struct field access and class-scope-typed struct members + Mid #(.W(8)) u_mid (); + PairHolder #(.cfg('{jt: '{cam_type: 8'd7, depth: 8'd3}, tag: 8'd11})) u_ph (); + + // (7) `localparam type` from a class typedef, bound to a type parameter + localparam type t_plain = pkg::C#(12)::data_t; + typedef pkg::C#(9) alias_c; + localparam type t_alias = alias_c::data_t; + TChild #(.T(t_plain), .EXP(12)) u_plain (.a_i('0)); + TChild #(.T(cfg_t), .EXP(24)) u_tstruct (.a_i('0)); + TChild #(.T(t_alias), .EXP(9)) u_alias (.a_i('0)); + TFwd #(.T(t_plain), .EXP(12)) u_fwd (); + + initial begin + `checkh(b3, 32'd3); + `checkh(b4, 32'd4); + `checkh(b5, 32'd5); + `checkh(b8, 32'd8); + `checkh(b13, 32'd13); + `checkh(c8_ref, 32'd8); + `checkh(d8_ref, 32'd9); + // (1) interface pins took the deferred widths + `checkh($bits(i_bare.data), 32'd8); + `checkh($bits(i_chain.data), 32'd8); + `checkh($bits(i_expr.data), 32'd21); + // (3) typedefs / $bits + `checkh(from_def_b, 32'd8); + `checkh(via_bits, 32'd8); + data_value = '1; + `checkh(data_value, 8'hff); + wide_bus = '1; + `checkh(wide_bus, 8'hff); + `checkh($bits(direct_use_t), 32'd16); + // (4) nested classes + `checkh(two_level, 32'd12); + `checkh(three_level, 32'd14); + `checkh(nested_chain, 32'd12); + `checkh(m_pin_sibling.WIDTH, 32'd13); + // (5) struct fields: tag = W+2, cam_type = W, depth = W+1 + `checkh(u_mid.u_tag.WIDTH, 32'd10); + `checkh(u_mid.u_cam.WIDTH, 32'd8); + `checkh(u_mid.u_depth.WIDTH, 32'd9); + `checkh(u_mid.u_bit.WIDTH, 32'd0); + `checkh(u_mid.u_part.WIDTH, 32'd4); + `checkh(u_mid.u_field_bit.WIDTH, 32'd1); + `checkh(u_mid.u_field_part.WIDTH, 32'd4); + `checkh(u_mid.u_array_field_part.WIDTH, 32'd4); + `checkh(u_mid.u_member_array_field_part.WIDTH, 32'd4); + `checkh(u_mid.u_q.WIDTH, 32'd110); + `checkh(u_ph.tag_val, 8'd11); + `checkh(u_ph.cam_val, 8'd7); + // (6) $bits(wrap_t) = 8 (payload) + 1 (v) = 9 + `checkh($bits(u_mid.u_sink.data), 32'd9); + // (7) localparam type widths + `checkh($bits(t_plain), 32'd12); + `checkh($bits(t_alias), 32'd9); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_class_lparam_generate_inline.py b/test_regress/t/t_class_lparam_generate_inline.py new file mode 100755 index 000000000..46d1fe4c0 --- /dev/null +++ b/test_regress/t/t_class_lparam_generate_inline.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_class_lparam_generate_inline.v b/test_regress/t/t_class_lparam_generate_inline.v new file mode 100644 index 000000000..fbadd78c8 --- /dev/null +++ b/test_regress/t/t_class_lparam_generate_inline.v @@ -0,0 +1,87 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Generate conditions that specialize a parameterized class *inline* +// (`C#(N)::b`, `$bits(C#(N))`) rather than through an intermediate +// localparam or typedef alias. +// +// V3Param's visit(AstGenIf/GenBlock/GenCase) iterates the condition +// before folding it, which queues the class reference in m_cellps via +// visitCellOrClassRef. Folding then destroys those nodes outright +// (V3Width deletes a resolved $bits() tree; the untaken arm is +// deleteTree()'d) instead of deferring via pushDeletep, so the queued +// pointers must be dropped or processWorkQ()'s drain loop dereferences +// freed memory -- giving a bogus "Expected module parameterization" +// fatal, or a segfault on the generate-case flavour. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkh(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0) +// verilog_format: on + +package P; + virtual class cfg #( + parameter int W = 8 + ); + localparam int width = W; + typedef logic [W-1:0] data_t; + endclass +endpackage + +// Cell whose presence is observable, to confirm each generate +// flavour elaborated the arm we expect. +module Tag #( + parameter int ID +) (); + initial $write("Tag ID=%0d\n", ID); +endmodule + +module t; + // Class in the compilation unit, reached with a single '::' + virtual class C #( + parameter int a + ); + localparam int b = a; + endclass + + // (1) Generate-if cond: inline specialization, single '::' + if (C#(5)::b > C#(3)::b) begin : gi_t + Tag #(200) inst (); + end else begin : gi_f + Tag #(201) inst (); + end + + // (2) Generate-if cond: inline specialization through a package, two '::' + if (P::cfg#(12)::width == 12) begin : gi_pkg + Tag #(212) inst (); + end + + // (3) Generate-if cond: $bits() of an inline specialization. No Dot at + // all -- the queued node is the AstClassRefDType itself, and V3Width + // deletes the whole $bits() tree once it folds. + if ($bits(P::cfg#(6)::data_t) == 6) begin : gi_bits + Tag #(206) inst (); + end + + // (4) Genvar bound = inline specialization + for (genvar i = 0; i < C#(3)::b; i++) begin : gf + Tag #(100 + i) inst (); + end + + // (5) Generate-case selector = inline specialization + case (P::cfg#(5)::width) + 3: begin : gc Tag #(303) inst (); end + 5: begin : gc Tag #(305) inst (); end + default: begin : gc Tag #(399) inst (); end + endcase + + initial begin + `checkh(C#(3)::b, 32'd3); + `checkh(P::cfg#(12)::width, 32'd12); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_class_lparam_nonparam_bad.out b/test_regress/t/t_class_lparam_nonparam_bad.out new file mode 100644 index 000000000..57dee0917 --- /dev/null +++ b/test_regress/t/t_class_lparam_nonparam_bad.out @@ -0,0 +1,21 @@ +%Error-UNSUPPORTED: t/t_class_lparam_nonparam_bad.v:26:14: dotted expressions in parameters + : ... note: In instance 't' + : ... Suggest use a typedef + 26 | Sub #(CFG::notaparam) u_prop (); + | ^~~~~~~~~ + ... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest +%Error: t/t_class_lparam_nonparam_bad.v:26:14: Can't convert defparam value to constant: Param '__paramNumber1' of 'u_prop' + : ... note: In instance 't' + 26 | Sub #(CFG::notaparam) u_prop (); + | ^~~~~~~~~ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Error-UNSUPPORTED: t/t_class_lparam_nonparam_bad.v:27:14: dotted expressions in parameters + : ... note: In instance 't' + : ... Suggest use a typedef + 27 | Sub #(CFG::alsonotaparam) u_static (); + | ^~~~~~~~~~~~~ +%Error: t/t_class_lparam_nonparam_bad.v:27:14: Can't convert defparam value to constant: Param '__paramNumber1' of 'u_static' + : ... note: In instance 't' + 27 | Sub #(CFG::alsonotaparam) u_static (); + | ^~~~~~~~~~~~~ +%Error: Exiting due to diff --git a/test_regress/t/t_class_lparam_nonparam_bad.py b/test_regress/t/t_class_lparam_nonparam_bad.py new file mode 100755 index 000000000..38cf36b43 --- /dev/null +++ b/test_regress/t/t_class_lparam_nonparam_bad.py @@ -0,0 +1,16 @@ +#!/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.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_class_lparam_nonparam_bad.v b/test_regress/t/t_class_lparam_nonparam_bad.v new file mode 100644 index 000000000..a4de1b0b6 --- /dev/null +++ b/test_regress/t/t_class_lparam_nonparam_bad.v @@ -0,0 +1,28 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Negative test for the member-kind guard in V3Param's +// substituteParamMember. A `class::member` Dot in a cell parameter pin +// that names a member which is not a parameter (a plain class property, or +// a static one) has no constant value to substitute, so V3Param must leave +// the Dot alone and let the normal unsupported-dotted-parameter diagnostic +// fire, rather than substituting or crashing. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +class C #(parameter int W = 1); + int notaparam; + static int alsonotaparam; + localparam int good = W; +endclass + +module Sub #(parameter int P = 0) (); +endmodule + +module t; + typedef C#(4) CFG; + + Sub #(CFG::notaparam) u_prop (); + Sub #(CFG::alsonotaparam) u_static (); +endmodule diff --git a/test_regress/t/t_class_lparam_repeat_ref.py b/test_regress/t/t_class_lparam_repeat_ref.py new file mode 100755 index 000000000..46d1fe4c0 --- /dev/null +++ b/test_regress/t/t_class_lparam_repeat_ref.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_class_lparam_repeat_ref.v b/test_regress/t/t_class_lparam_repeat_ref.v new file mode 100644 index 000000000..c4cb137f2 --- /dev/null +++ b/test_regress/t/t_class_lparam_repeat_ref.v @@ -0,0 +1,59 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Repeated references to the same deferred localparam. V3Param's +// resolveDeferredDotsReachableFrom walks a pin expression collecting +// deferred lparams to descend into, and must visit each one only once even +// when a single expression (or several pins on the same cell) references it +// many times, including via a diamond where two intermediate lparams both +// lead back to the same deferred one. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkh(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0) +// verilog_format: on + +class Inner #(parameter int V = 1); + localparam int v = V; +endclass + +module Sub #( + parameter int P = 0, + parameter int Q = 0 +) (); + localparam int GOTP = P; + localparam int GOTQ = Q; +endmodule + +module t; + // Deferred: value is a class::member Dot. + localparam int d = Inner#(7)::v; + + // Diamond: both lead back to the single deferred `d`. + localparam int left = d + 1; + localparam int right = d + 2; + + // Same deferred lparam reached repeatedly within one pin expression... + Sub #( + .P(d + d + d), + .Q(left + right) + ) u_repeat (); + + // ...and across two pins of the same cell. + Sub #( + .P(d), + .Q(d) + ) u_bothpins (); + + initial begin + `checkh(u_repeat.GOTP, 32'd21); + `checkh(u_repeat.GOTQ, 32'd17); + `checkh(u_bothpins.GOTP, 32'd7); + `checkh(u_bothpins.GOTQ, 32'd7); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_class_lparam_sibling_chain.py b/test_regress/t/t_class_lparam_sibling_chain.py new file mode 100755 index 000000000..46d1fe4c0 --- /dev/null +++ b/test_regress/t/t_class_lparam_sibling_chain.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_class_lparam_sibling_chain.v b/test_regress/t/t_class_lparam_sibling_chain.v new file mode 100644 index 000000000..c528a5621 --- /dev/null +++ b/test_regress/t/t_class_lparam_sibling_chain.v @@ -0,0 +1,60 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Sibling-localparam recursion in V3Param's constifyMemberValue. When a +// `class::member` Dot resolves to an lparam whose own value is not yet a +// Const because it references *sibling* lparams of the same class, those +// siblings must be folded first (deepest-first) so the whole chain +// constifies. Covers values that reach the class::member Dot only +// indirectly, e.g. `one = base` where `base = Inner#(W)::v`, including +// multi-level chains and one expression referencing several siblings. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkh(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0) +// verilog_format: on + +class Inner #(parameter int V = 1); + localparam int v = V; +endclass + +class C #(parameter int W = 1); + // Only `base` holds the class::member Dot directly. + localparam int base = Inner#(W)::v; + // `one` is a bare VarRef to a sibling that is not yet Const. + localparam int one = base; + // Multi-level: two -> one -> base -> Inner::v + localparam int two = one + 1; + // One value referencing several not-yet-Const siblings at once. + localparam int sum = base + one + two; + // A non-param member: substituteParamMember must leave it alone. + int notaparam; +endclass + +module Sub #( + parameter int P = 0 +) (); + localparam int GOT = P; +endmodule + +module t; + typedef C#(5) CFG; + + // Each pin drags a different point of the sibling chain through V3Param. + Sub #(CFG::base) u_base (); + Sub #(CFG::one) u_one (); + Sub #(CFG::two) u_two (); + Sub #(CFG::sum) u_sum (); + + initial begin + `checkh(u_base.GOT, 32'd5); + `checkh(u_one.GOT, 32'd5); + `checkh(u_two.GOT, 32'd6); + `checkh(u_sum.GOT, 32'd16); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_class_lparam_struct_field_chain_bad.out b/test_regress/t/t_class_lparam_struct_field_chain_bad.out new file mode 100644 index 000000000..abe85d45f --- /dev/null +++ b/test_regress/t/t_class_lparam_struct_field_chain_bad.out @@ -0,0 +1,19 @@ +%Error-UNSUPPORTED: t/t_class_lparam_struct_field_chain_bad.v:36:27: Unsupported: Member call on object 'SEL' which is a 'BASICDTYPE 'logic'' + : ... note: In instance 't.u' + 36 | Sub #(int'(CFG::cfg.tag.bogus)) u_nonstruct (); + | ^~~~~ + ... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest +%Error-UNSUPPORTED: t/t_class_lparam_struct_field_chain_bad.v:36:12: Unsupported: static cast to 'int' from 'VOIDDTYPE' + : ... note: In instance 't.u' + 36 | Sub #(int'(CFG::cfg.tag.bogus)) u_nonstruct (); + | ^ +%Error: t/t_class_lparam_struct_field_chain_bad.v:36:12: Can't convert defparam value to constant: Param '__paramNumber1' of 'u_nonstruct' + : ... note: In instance 't.u' + 36 | Sub #(int'(CFG::cfg.tag.bogus)) u_nonstruct (); + | ^ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Error: t/t_class_lparam_struct_field_chain_bad.v:38:26: Member 'nosuchfield' not found in structure + : ... note: In instance 't.u' + 38 | Sub #(int'(CFG::cfg.jt.nosuchfield)) u_nomember (); + | ^~~~~~~~~~~ +%Error: Exiting due to diff --git a/test_regress/t/t_class_lparam_struct_field_chain_bad.py b/test_regress/t/t_class_lparam_struct_field_chain_bad.py new file mode 100755 index 000000000..38cf36b43 --- /dev/null +++ b/test_regress/t/t_class_lparam_struct_field_chain_bad.py @@ -0,0 +1,16 @@ +#!/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.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_class_lparam_struct_field_chain_bad.v b/test_regress/t/t_class_lparam_struct_field_chain_bad.v new file mode 100644 index 000000000..2c16a5493 --- /dev/null +++ b/test_regress/t/t_class_lparam_struct_field_chain_bad.v @@ -0,0 +1,43 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Negative test for the struct-localparam field-access folding in V3Param +// (resolveDotToTypedef). Malformed outer `.field` chains on a class +// localparam value must produce clean user-facing errors, NOT an internal +// error or a crash in the following width pass. Exercises the two +// mismatch guards in the field-chain walk: +// (1) `.field` applied to a non-struct value +// (2) field name not a member of the struct +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +typedef struct packed { + logic [7:0] cam_type; + logic [7:0] depth; +} inner_t; + +typedef struct packed { + inner_t jt; + logic [7:0] tag; +} cfg_t; + +class C #(parameter int W = 1); + localparam cfg_t cfg = '{jt: '{cam_type: W[7:0], depth: W[7:0] + 8'd1}, + tag: W[7:0] + 8'd2}; +endclass + +module Sub #(parameter int WIDTH = 0) (); +endmodule + +module Mid #(parameter int W = 8) (); + typedef C#(W) CFG; + // (1) `.bogus` on the scalar field `tag`: dotting into a non-struct + Sub #(int'(CFG::cfg.tag.bogus)) u_nonstruct (); + // (2) `.nosuchfield` not a member of struct `jt` + Sub #(int'(CFG::cfg.jt.nosuchfield)) u_nomember (); +endmodule + +module t; + Mid #(.W(8)) u (); +endmodule diff --git a/test_regress/t/t_class_lparam_type_param.py b/test_regress/t/t_class_lparam_type_param.py new file mode 100755 index 000000000..07af67e72 --- /dev/null +++ b/test_regress/t/t_class_lparam_type_param.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', '--assert']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_class_lparam_type_param.v b/test_regress/t/t_class_lparam_type_param.v new file mode 100644 index 000000000..360efb216 --- /dev/null +++ b/test_regress/t/t_class_lparam_type_param.v @@ -0,0 +1,48 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +class types_c #( + parameter int W = 8 +); + typedef logic [W-1:0] t; + typedef logic signed [W-1:0] type1_t; + typedef type1_t [W-1:0] type2_t; +endclass + +module type_scope #( + parameter type C = types_c#(8) +) (); + C::t value; + child #(.T(C::t)) u_child (.a_i('0)); + initial assert ($bits(value) == 8); +endmodule + +module child #( + parameter type T = logic +) ( + input T a_i +); +endmodule + +module t; + localparam int W = 8; + + // Class-scoped type via a localparam type, passed to a child type parameter. + localparam type t_param = types_c#(W)::t; + child #(.T(t_param)) u (.a_i('0)); + type_scope u_type_scope (); + + // Class-scoped type via a module-body typedef alias of a nested typedef. + typedef types_c#(W)::type2_t t_alias; + t_alias v; + + initial begin + v = '0; + assert ($bits(t_param) == 8); + assert ($bits(t_alias) == 64); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_class_param_struct.py b/test_regress/t/t_class_param_struct.py new file mode 100755 index 000000000..6fe7d000c --- /dev/null +++ b/test_regress/t/t_class_param_struct.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_class_param_struct.v b/test_regress/t/t_class_param_struct.v new file mode 100644 index 000000000..ec41c732f --- /dev/null +++ b/test_regress/t/t_class_param_struct.v @@ -0,0 +1,90 @@ +// DESCRIPTION: Verilator: Unpacked struct values as class parameters +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkh(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0) +// verilog_format: on + +package P; + typedef struct { + int depth; + } memory_config_t; + + typedef struct { + memory_config_t memory; + int counters; + } config_t; + + virtual class cfg #( + parameter config_t C + ); + localparam int WIDTH = $clog2(C.memory.depth); + localparam int COUNTERS = C.counters; + typedef logic [WIDTH-1:0] data_t; + endclass + + localparam config_t PKG_CFG = '{memory: '{depth: 8192}, counters: 6}; + typedef cfg#(PKG_CFG) PkgCfg; +endpackage + +package Q; + localparam P::config_t CFG = '{memory: '{depth: 1024}, counters: 3}; + typedef P::cfg#(CFG) CrossPkgCfg; +endpackage + +module Sub #( + parameter P::config_t C +) ( + input P::cfg#(C)::data_t din +); + initial begin + `checkh($bits(din), $clog2(C.memory.depth)); + `checkh(C.counters, P::cfg#(C)::COUNTERS); + end +endmodule + +module t; + localparam P::config_t CFG_A = '{memory: '{depth: 4096}, counters: 4}; + localparam P::config_t CFG_B = '{memory: '{depth: 32768}, counters: 7}; + // Equal to CFG_A, so must select the same Sub specialization. + localparam P::config_t CFG_A2 = '{memory: '{depth: 4096}, counters: 4}; + // Differs from CFG_A only in a nested field, so must not share with it. + localparam P::config_t CFG_C = '{memory: '{depth: 4096}, counters: 5}; + + P::cfg#(CFG_A)::data_t a; + P::cfg#(CFG_B)::data_t b; + P::PkgCfg::data_t pkg; + Q::CrossPkgCfg::data_t cross_pkg; + + P::cfg#(CFG_A2)::data_t a2; + P::cfg#(CFG_C)::data_t c; + + Sub #(.C(CFG_A)) sub (.din(a)); + Sub #(.C(CFG_A2)) sub_same (.din(a2)); + Sub #(.C(CFG_C)) sub_diff (.din(c)); + + initial begin + a = '0; + b = '0; + pkg = '0; + cross_pkg = '0; + a2 = '0; + c = '0; + `checkh($bits(a), 12); + `checkh($bits(b), 15); + `checkh($bits(pkg), 13); + `checkh($bits(cross_pkg), 10); + `checkh(P::cfg#(CFG_A)::COUNTERS, 4); + `checkh(P::cfg#(CFG_B)::COUNTERS, 7); + // Equal struct values must specialize identically, unequal ones distinctly. + `checkh($bits(a2), $bits(a)); + `checkh(P::cfg#(CFG_A2)::COUNTERS, 4); + `checkh(P::cfg#(CFG_C)::COUNTERS, 5); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_class_param_struct_collision.py b/test_regress/t/t_class_param_struct_collision.py new file mode 100755 index 000000000..ad6297ddd --- /dev/null +++ b/test_regress/t/t_class_param_struct_collision.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_class_param_struct.v" + +test.compile(verilator_flags2=["--binary --debug-collision"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_class_scope_chain.py b/test_regress/t/t_class_scope_chain.py new file mode 100755 index 000000000..46d1fe4c0 --- /dev/null +++ b/test_regress/t/t_class_scope_chain.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_class_scope_chain.v b/test_regress/t/t_class_scope_chain.v new file mode 100644 index 000000000..ee8b034cb --- /dev/null +++ b/test_regress/t/t_class_scope_chain.v @@ -0,0 +1,120 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Chained class scope resolution in *type* position: `pkg::cls::t` and +// `pkg::cls#(P)::t`. +// +// V3LinkDot's visit(AstRefDType) resolves the scope operand, but only handled a +// single AstClassOrPackageRef. With two '::' the operand is an AstDot holding a +// ref per scope, which fell to an E_UNSUPPORTED ("Multiple '::' package/class +// reference") followed by "Can't find typedef". Value position (`pkg::cls::n` +// in a localparam) already worked -- only the type path was missing. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkh(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0) +// verilog_format: on + +package P; + class Plain; + typedef logic [7:0] t; + localparam int n = 8; + endclass + + class Par #( + parameter int W = 8 + ); + typedef logic [W-1:0] t; + localparam int n = W; + endclass + + class Outer; + class Inner; + typedef logic [6:0] t; + endclass + endclass + + // Traits-class shape: types derived from a struct config parameter + typedef struct packed { + int depth; + int counters; + } config_t; + + virtual class cfg #( + parameter config_t c + ); + localparam int width = $clog2(c.depth); + localparam int counters = c.counters; + typedef logic [width-1:0] data_t; + endclass +endpackage + +// Class-scoped types crossing a module boundary, as a port and inside a +// packed struct alongside type-parameter members. +module Sub #( + parameter P::config_t cfg, + parameter type stk_t = logic +) ( + input P::cfg#(cfg)::data_t din, + input logic [P::cfg#(cfg)::counters-1:0] mask, + input stk_t stk, + output P::cfg#(cfg)::data_t dout +); + typedef struct packed { + P::cfg#(cfg)::data_t d; + stk_t s; + } packed_t; + packed_t p; + always_comb p = '{d: din, s: stk}; + always_comb dout = p.d; + initial begin + `checkh($bits(din), 12); + `checkh($bits(mask), 4); + end +endmodule + +module t; + // (1) pkg::cls::type -- no parameters + P::Plain::t a; + // (2) pkg::cls#(P)::type -- parameterized + P::Par#(12)::t b; + // (3) through a typedef + typedef P::Par#(5)::t c_t; + c_t c; + // (4) an arbitrarily deep class scope + P::Outer::Inner::t d; + // (5) value position (already worked; guards against regressing it) + localparam int na = P::Plain::n; + localparam int nb = P::Par#(12)::n; + + localparam P::config_t CFG = '{depth: 4096, counters: 4}; + typedef struct packed { logic [7:0] hi; } stk_t; + P::cfg#(CFG)::data_t din, dout; + + Sub #(.cfg(CFG), .stk_t(stk_t)) sub ( + .din (din), + .mask ('0), + .stk ('0), + .dout (dout) + ); + + initial begin + a = '0; + b = '0; + c = '0; + d = '0; + din = '0; + `checkh($bits(a), 8); + `checkh($bits(b), 12); + `checkh($bits(c), 5); + `checkh($bits(d), 7); + `checkh($bits(dout), 12); + `checkh(na, 32'd8); + `checkh(nb, 32'd12); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_class_scope_chain_alias_bad.out b/test_regress/t/t_class_scope_chain_alias_bad.out new file mode 100644 index 000000000..0e97075f7 --- /dev/null +++ b/test_regress/t/t_class_scope_chain_alias_bad.out @@ -0,0 +1,5 @@ +%Error-UNSUPPORTED: t/t_class_scope_chain_alias_bad.v:38:6: Unsupported: Multiple '::' package/class reference + 38 | Q::AliasPC::inner::u e; + | ^~~~~~~ + ... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest +%Error: Exiting due to diff --git a/test_regress/t/t_class_scope_chain_alias_bad.py b/test_regress/t/t_class_scope_chain_alias_bad.py new file mode 100755 index 000000000..38cf36b43 --- /dev/null +++ b/test_regress/t/t_class_scope_chain_alias_bad.py @@ -0,0 +1,16 @@ +#!/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.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_class_scope_chain_alias_bad.v b/test_regress/t/t_class_scope_chain_alias_bad.v new file mode 100644 index 000000000..f92f7250c --- /dev/null +++ b/test_regress/t/t_class_scope_chain_alias_bad.v @@ -0,0 +1,48 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// A middle segment of a `::` chain that aliases a parameterized class which is +// not yet specialized has no reachable module to resolve the following segment +// in. This is legal SystemVerilog that Verilator does not yet support, so it +// must report gracefully rather than hit an internal assertion. +// +// Kept separate from t_class_scope_chain_bad.v because the errors there abort +// the pass before a later declaration would be reached. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +package P; + typedef struct packed { + int depth; + } config_t; + + virtual class PC #( + parameter config_t c + ); + localparam int width = $clog2(c.depth); + virtual class inner; + typedef logic [width-1:0] u; + endclass + endclass +endpackage + +package Q; + localparam P::config_t cfgv = '{depth: 16}; + typedef P::PC#(cfgv) AliasPC; +endpackage + +// Referencing from an instantiated submodule is what leaves the alias +// unspecialized at the time the chain is resolved. +module Leaf (); + Q::AliasPC::inner::u e; + initial if ($bits(e) != 4) $stop; +endmodule + +module t; + Leaf leaf (); + initial begin + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_class_scope_chain_bad.out b/test_regress/t/t_class_scope_chain_bad.out new file mode 100644 index 000000000..de2454151 --- /dev/null +++ b/test_regress/t/t_class_scope_chain_bad.out @@ -0,0 +1,14 @@ +%Error: t/t_class_scope_chain_bad.v:26:9: Can't find typedef/interface: 'nosuch' + 26 | P::K::nosuch a; + | ^~~~~~ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Error: t/t_class_scope_chain_bad.v:28:6: Package/class for 'class/package reference' not found: 'NoSuchClass' + 28 | P::NoSuchClass::t b; + | ^~~~~~~~~~~ +%Error: t/t_class_scope_chain_bad.v:30:3: Package/class for 'class/package reference' not found: 'NoSuchPkg' + 30 | NoSuchPkg::K::t c; + | ^~~~~~~~~ +%Error: t/t_class_scope_chain_bad.v:32:9: Package/class for 'class/package reference' not found: 't' + 32 | P::K::t::deeper d; + | ^ +%Error: Exiting due to diff --git a/test_regress/t/t_class_scope_chain_bad.py b/test_regress/t/t_class_scope_chain_bad.py new file mode 100755 index 000000000..38cf36b43 --- /dev/null +++ b/test_regress/t/t_class_scope_chain_bad.py @@ -0,0 +1,16 @@ +#!/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.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_class_scope_chain_bad.v b/test_regress/t/t_class_scope_chain_bad.v new file mode 100644 index 000000000..270aeb5dd --- /dev/null +++ b/test_regress/t/t_class_scope_chain_bad.v @@ -0,0 +1,37 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Error cases for chained class scope resolution in type position. Each stage +// of `pkg::cls::t` can fail to resolve; the diagnostic should name the segment +// that actually failed, and should not also emit the "Multiple '::'" message. +// +// Arbitrarily deep nesting is supported; if an intermediate segment is not a +// class or package, it should receive the same lookup diagnostic as any other +// unresolved scope. +// +// Each declaration reports exactly once: once a segment fails, resolution of +// that reference stops rather than also reporting the trailing type name. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +package P; + class K; + typedef logic [7:0] t; + endclass +endpackage + +module t; + // Inner name is not a member of the (resolved) class + P::K::nosuch a; + // Middle scope does not exist + P::NoSuchClass::t b; + // Outer scope does not exist + NoSuchPkg::K::t c; + // Deeper than one nested scope is still unsupported + P::K::t::deeper d; + initial begin + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_class_scope_pkg_alias.py b/test_regress/t/t_class_scope_pkg_alias.py new file mode 100755 index 000000000..46d1fe4c0 --- /dev/null +++ b/test_regress/t/t_class_scope_pkg_alias.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_class_scope_pkg_alias.v b/test_regress/t/t_class_scope_pkg_alias.v new file mode 100644 index 000000000..47ae83d72 --- /dev/null +++ b/test_regress/t/t_class_scope_pkg_alias.v @@ -0,0 +1,79 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// `outer_pkg::Alias::member`, where `Alias` is a typedef in `outer_pkg` that +// aliases a parameterized class from another package. +// +// Until the class is specialized in V3Param the alias's RHS is still a +// RefDType carrying its own scope, so classOrPackageSkipp() walks to null. +// Code that tested only Skipp therefore treated the (already resolved) ref as +// unresolved and re-looked-up its bare name in the referencing module's scope, +// where `Alias` is not visible - reporting "Package/class for ':: reference' +// not found". Resolving from a submodule is what exposes it; with the +// referencing module as top the enclosing scope happens to still find it. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkh(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0) +// verilog_format: on + +package base_pkg; + typedef struct packed { + int depth; + int meta; + } config_t; + + virtual class cfg #( + parameter config_t c + ); + localparam int width = $clog2(c.depth); + typedef logic [width-1:0] data_t; + typedef struct packed { + logic [c.meta-1:0] m; + data_t d; + } meta_t; + endclass +endpackage + +package top_pkg; + localparam base_pkg::config_t cfgv = '{depth: 512, meta: 4}; + // Typedef alias of a parameterized class, in a different package + typedef base_pkg::cfg #(cfgv) AliasCFG; +endpackage + +// Alias-scoped type as a port, and in the body, of an instantiated submodule +module Leaf ( + input top_pkg::AliasCFG::meta_t in_meta, + output top_pkg::AliasCFG::data_t out_data +); + top_pkg::AliasCFG::meta_t body_meta; + always_comb begin + body_meta = in_meta; + out_data = body_meta.d; + end + initial begin + `checkh($bits(in_meta), 13); + `checkh($bits(out_data), 9); + end +endmodule + +module Mid (); + top_pkg::AliasCFG::meta_t m; + top_pkg::AliasCFG::data_t d; + Leaf leaf ( + .in_meta (m), + .out_data (d) + ); + initial m = '0; +endmodule + +module t; + Mid mid (); + initial begin + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_class_type_dep_param.py b/test_regress/t/t_class_type_dep_param.py new file mode 100755 index 000000000..46d1fe4c0 --- /dev/null +++ b/test_regress/t/t_class_type_dep_param.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_class_type_dep_param.v b/test_regress/t/t_class_type_dep_param.v new file mode 100644 index 000000000..a2d9f1418 --- /dev/null +++ b/test_regress/t/t_class_type_dep_param.v @@ -0,0 +1,112 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// `$bits()` of a signal whose type comes from a parameterized class +// (`CFG::data_t`, via a typedef alias of `cls#(P)`). +// +// V3LinkDot defers linking a RefDType whose scope is a parameterized class - +// it can only resolve once the class is specialized in V3Param. V3Param +// resolves such RefDTypes where it knows to look (cell pin types, typedefs +// reachable from a pin), but a plain module-level signal is not one of those +// places; its RefDType stays unlinked until V3Width runs normally. +// +// `$bits(sig)` breaks that: constifyParamsEdit -> widthParamsEdit runs +// V3Width early, during V3Param, to fold the parameter expression. Widthing +// the VarRef forces the Var to be widthed, which calls skipRefp() on the +// still-unlinked RefDType, giving "REFDTYPE 'data_t' not linked to type". +// Using the signal ordinarily (assignment) does not trigger it, because +// nothing forces the early width pass. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkh(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0) +// verilog_format: on + +package pkg; + typedef struct packed { + int depth; + } config_t; + + virtual class cfg #( + parameter config_t c + ); + localparam int width = $clog2(c.depth); + typedef logic [width-1:0] data_t; + endclass +endpackage + +// Child with a type parameter defaulted from another parameter, as in a +// sample-and-hold style utility module +module Holder #( + parameter int width = 0, + parameter type data_t = logic [width-1:0] +) ( + input data_t dat_i, + output data_t dat_o +); + always_comb dat_o = dat_i; +endmodule + +module Sub #( + parameter pkg::config_t cfg +) (); + typedef pkg::cfg #(cfg) CFG; + CFG::data_t src, dst; + typedef CFG::data_t unpacked_t[3:1]; + typedef CFG::data_t dynamic_t[]; + typedef CFG::data_t queue_t[$]; + typedef CFG::data_t associative_t[int]; + unpacked_t unpacked_data; + dynamic_t dynamic_data; + queue_t queue_data; + associative_t associative_data; + + // (1) $bits() of a class-scoped-typed signal, in a localparam + localparam int Bits = $bits(src); + localparam int UnpackedBits = $bits(unpacked_data); + + // (2) $bits() of a class-scoped-typed signal, as a cell parameter pin + Holder #(.width($bits(src))) holder ( + .dat_i (src), + .dat_o (dst) + ); + + // (3) Same, inside a generate arm + if (CFG::width > 0) begin : gen_arm + CFG::data_t arm_src, arm_dst; + Holder #(.width($bits(arm_src))) armHolder ( + .dat_i (arm_src), + .dat_o (arm_dst) + ); + initial arm_src = '0; + end + + initial begin + src = '0; + unpacked_data = '{default: '1}; + dynamic_data = new[1]; + dynamic_data[0] = '1; + queue_data.push_back('1); + associative_data[3] = '1; + `checkh(Bits, 32'd9); + `checkh(UnpackedBits, 32'd27); + `checkh($bits(src), 32'd9); + `checkh($bits(dst), 32'd9); + `checkh($bits(dynamic_data[0]), 32'd9); + `checkh($bits(queue_data[0]), 32'd9); + `checkh($bits(associative_data[3]), 32'd9); + end +endmodule + +module t; + localparam pkg::config_t CFGV = '{depth: 512}; + Sub #(.cfg(CFGV)) sub (); + + initial begin + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_parse_sync_bad2.out b/test_regress/t/t_parse_sync_bad2.out index 1352669f2..2a1588e60 100644 --- a/test_regress/t/t_parse_sync_bad2.out +++ b/test_regress/t/t_parse_sync_bad2.out @@ -5,19 +5,6 @@ %Error: t/t_parse_sync_bad2.v:17:7: Can't find typedef/interface: 'Invalid1' 17 | Invalid1 invalid1; | ^~~~~~~~ -%Error-UNSUPPORTED: t/t_parse_sync_bad2.v:18:12: Unsupported: Multiple '::' package/class reference - 18 | pkg::cls::defi valid1; - | ^~~ - ... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest -%Error: t/t_parse_sync_bad2.v:18:17: Can't find typedef/interface: 'defi' - 18 | pkg::cls::defi valid1; - | ^~~~ -%Error-UNSUPPORTED: t/t_parse_sync_bad2.v:19:12: Unsupported: Multiple '::' package/class reference - 19 | pkg::cls::defu valid2; - | ^~~ -%Error: t/t_parse_sync_bad2.v:19:17: Can't find typedef/interface: 'defu' - 19 | pkg::cls::defu valid2; - | ^~~~ %Error: t/t_parse_sync_bad2.v:20:7: Can't find typedef/interface: 'Invalid2' 20 | Invalid2 invalid2; | ^~~~~~~~ diff --git a/test_regress/t/t_struct_param_public.py b/test_regress/t/t_struct_param_public.py new file mode 100755 index 000000000..5f3a1e14a --- /dev/null +++ b/test_regress/t/t_struct_param_public.py @@ -0,0 +1,21 @@ +#!/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') + +# --public-params makes the struct parameter's members get registered for VPI, +# which previously emitted a 'const void*' -> 'void*' conversion and so failed +# to compile the generated C++. +test.compile(verilator_flags2=["--binary --public-params"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_struct_param_public.v b/test_regress/t/t_struct_param_public.v new file mode 100644 index 000000000..a9f698266 --- /dev/null +++ b/test_regress/t/t_struct_param_public.v @@ -0,0 +1,38 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +// verilator lint_off UNUSEDPARAM + +package P; + typedef struct { + int depth; + } memory_config_t; + + typedef struct { + memory_config_t memory; + int counters; + } config_t; +endpackage + +module t; + // A public unpacked-struct parameter is emitted 'static const', so the + // registration of its members must cast away const like the whole + // parameter's registration does. + localparam P::config_t CFG = '{memory: '{depth: 8192}, counters: 6}; + // An unpacked array of structs registers members through a separate path, + // which used to null-dereference when asking whether it is a literal type. + localparam P::memory_config_t MEM_ARR[2] = '{'{depth: 16}, '{depth: 32}}; + + initial begin + if (CFG.memory.depth != 8192) $stop; + if (CFG.counters != 6) $stop; + // Selected at runtime; indexing an array-of-structs inside a constant + // expression is a separate, still-unsupported case. + if (MEM_ARR[1].depth != 32) $stop; + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule