Fix deferring parameters for class::localparam access (#7746) (#7748)

This commit is contained in:
Edmund Lam
2026-09-15 21:17:26 -04:00
committed by GitHub
parent c9e3f59f26
commit 529d98918a
45 changed files with 1940 additions and 91 deletions
+2
View File
@@ -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
+3 -1
View File
@@ -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()'
+11 -5
View File
@@ -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<void*>(static_cast<const void*>(&(";
stmt += varName;
stmt += "))), true, ";
} else {
stmt += ", &(";
stmt += varName;
stmt += "), false, ";
}
const std::string entSize
= needsEntSize
? "sizeof(" + varName + ") / " + std::to_string(getUnpackedElements(dtypep))
+71 -12
View File
@@ -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<AstClassOrPackageRef*>& 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<AstClassOrPackageRef*> 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);
}
+258 -60
View File
@@ -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<AstVar*>& 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<AstVar*> 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<AstVar*> 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<const AstNode*> 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<AstDot*> 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;
}
@@ -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
+16
View File
@@ -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()
@@ -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
@@ -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
+16
View File
@@ -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()
+32
View File
@@ -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
+18
View File
@@ -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()
+294
View File
@@ -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
+18
View File
@@ -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()
@@ -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
@@ -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
+16
View File
@@ -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()
@@ -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
+18
View File
@@ -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()
@@ -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
+18
View File
@@ -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()
@@ -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
@@ -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
+16
View File
@@ -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()
@@ -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
+18
View File
@@ -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()
@@ -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
+18
View File
@@ -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()
+90
View File
@@ -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
+19
View File
@@ -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()
+18
View File
@@ -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()
+120
View File
@@ -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
@@ -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
+16
View File
@@ -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()
@@ -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
@@ -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
+16
View File
@@ -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()
+37
View File
@@ -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
+18
View File
@@ -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()
+79
View File
@@ -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
+18
View File
@@ -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()
+112
View File
@@ -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
-13
View File
@@ -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;
| ^~~~~~~~
+21
View File
@@ -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()
+38
View File
@@ -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