diff --git a/src/V3Param.cpp b/src/V3Param.cpp index fac1c757e..8c6d65fab 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -2367,6 +2367,22 @@ class ParamClassRefDTypeRelinkVisitor final : public VNVisitor { } } + // A class's name before specialization (e.g. "holder" for "holder__Tz1"). + static string classOrigName(const AstClass* classp) { + return classp->origName().empty() ? classp->name() : classp->origName(); + } + + // Find 'name' in classp or any base class (findTypedefInModule() searches + // only the class itself). + static AstTypedef* findTypedefWithBases(AstClass* classp, const string& name) { + for (AstClass* cp = classp; cp; cp = cp->extendsp() ? cp->extendsp()->classp() : nullptr) { + if (AstTypedef* const tdp = V3LinkDotIfaceCapture::findTypedefInModule(cp, name)) { + if (tdp->subDTypep()) return tdp; + } + } + return nullptr; + } + // Re-resolve REFDTYPE.typedefp/refDTypep using the containing module's // own resolved typedef chain. The eager retargeting in deepCloneModule // blindly retargets every captured entry to whichever sibling clone is @@ -2377,10 +2393,46 @@ class ParamClassRefDTypeRelinkVisitor final : public VNVisitor { // those local typedefs as ground truth. void retargetRefDType(AstRefDType* refp) { if (!m_ownerModp) return; + + // IEEE 1800-2023 8.25.1: a bare class-qualified reference to the class + // being compiled means the current specialization, but V3LinkDot binds + // it to the default instance, so a typedef reached through it widens + // with the template's defaults (#8348). Rebind it to the + // specialization's own typedef. + if (AstClass* const ownerClassp = VN_CAST(m_ownerModp, Class)) { + if (!ownerClassp->hasGParam() && !refp->paramsp()) { + // What the reference resolves to: its typedef's owner if + // linked, else the class it is qualified by. + const AstClass* refClassp = nullptr; + if (AstTypedef* const tdp = refp->typedefp()) { + refClassp = VN_CAST(V3LinkDotIfaceCapture::findOwnerModule(tdp), Class); + } else if (const AstClassOrPackageRef* const classRefp + = VN_CAST(refp->classOrPackageOpp(), ClassOrPackageRef)) { + refClassp = VN_CAST(classRefp->classOrPackageSkipp(), Class); + } + // Same originating class => bare self reference (another + // specialization would carry #(), rejected above). + if (refClassp && refClassp != ownerClassp + && classOrigName(refClassp) == classOrigName(ownerClassp)) { + AstTypedef* const selfTdp = findTypedefWithBases(ownerClassp, refp->name()); + if (selfTdp) { + UINFO(9, "post-param REFDTYPE self-reference retarget: " + << refp << " from " << refClassp->name() << " to " + << ownerClassp->name()); + refp->typedefp(selfTdp); + refp->classOrPackagep(V3LinkDotIfaceCapture::findOwnerModule(selfTdp)); + refp->refDTypep(selfTdp->subDTypep()); + return; + } + } + } + } + AstTypedef* const oldTdp = refp->typedefp(); if (!oldTdp) return; AstClass* const oldOwnerp = VN_CAST(V3LinkDotIfaceCapture::findOwnerModule(oldTdp), Class); if (!oldOwnerp) return; + ensureOwnerMap(); if (m_origNameToClone.empty()) return; const std::string origName diff --git a/test_regress/t/t_class_param_self_ref_typedef.py b/test_regress/t/t_class_param_self_ref_typedef.py new file mode 100755 index 000000000..8a938befd --- /dev/null +++ b/test_regress/t/t_class_param_self_ref_typedef.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile() + +test.execute() + +test.passes() diff --git a/test_regress/t/t_class_param_self_ref_typedef.v b/test_regress/t/t_class_param_self_ref_typedef.v new file mode 100644 index 000000000..06310e1ef --- /dev/null +++ b/test_regress/t/t_class_param_self_ref_typedef.v @@ -0,0 +1,65 @@ +// 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 + +// IEEE 1800-2023 8.25.1: inside a parameterized class, a bare class name used +// without #() denotes the current specialization. A typedef reached through +// such a self reference must resolve against the specialization's type +// parameters, not the template's defaults. Covers both a typedef declared in +// the class itself and one inherited from a base class. + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d (%s !== %s)\n", `__FILE__,`__LINE__, (gotv), (expv), `"gotv`", `"expv`"); `stop; end while(0); +// verilog_format: on + +package pkg; + class item_base #(type DATA = int); + DATA value; + endclass + + class holder #(type FIN = int); + typedef item_base#(FIN) my_item_t; + + virtual function holder::my_item_t get_bare(); + my_item_t item; + return item; + endfunction + endclass + + // Same, but the typedef is inherited from a base class instead. + class base_holder #(type FIN = int); + typedef item_base#(FIN) my_item_t; + endclass + + class derived #(type FIN = int) extends base_holder#(FIN); + virtual function derived::my_item_t get_bare(); + my_item_t item; + return item; + endfunction + endclass +endpackage + +module t; + import pkg::*; + + // Non-default parameters, so the specialization differs from the template + // default and the bug is observable. + holder#(byte) h; + derived#(shortint) d; + + initial begin + automatic holder#(byte)::my_item_t got; + automatic derived#(shortint)::my_item_t got_inherited; + h = new; + got = h.get_bare(); + `checkd($bits(got.value), 8); + d = new; + got_inherited = d.get_bare(); + `checkd($bits(got_inherited.value), 16); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule