Fix nested parameterized class typedef chain (#7538)

This commit is contained in:
Michael Rogenmoser 2026-05-19 20:12:24 +02:00 committed by GitHub
parent b06ea01afb
commit cb3b9c7c43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 285 additions and 20 deletions

View File

@ -2303,11 +2303,120 @@ class ParamClassRefDTypeRelinkVisitor final : public VNVisitor {
AstClass* m_classp = nullptr;
std::unordered_map<const AstParamTypeDType*, AstClass*> m_paramTypeClassMap;
// Owner-tracking state for the post-clone REFDTYPE typedef retargeting
// (see retargetRefDType()). m_ownerModp is the most recently entered
// AstNodeModule (or AstClass). m_origNameToClone / m_ambiguous are
// rebuilt lazily on the first AstRefDType visit per owner; the
// m_ownerMapForp cache key avoids rebuilding when the owner is unchanged
// (e.g. across siblings under the same parent).
AstNodeModule* m_ownerModp = nullptr;
AstNodeModule* m_ownerMapForp = nullptr;
std::unordered_map<std::string, AstClass*> m_origNameToClone;
std::unordered_set<std::string> m_ambiguous;
void ensureOwnerMap() {
if (m_ownerMapForp == m_ownerModp) return;
m_ownerMapForp = m_ownerModp;
m_origNameToClone.clear();
m_ambiguous.clear();
if (!m_ownerModp) return;
// Skip bare/generic class templates: their bodies are not
// authoritative for any specialization, and their self-typedefs
// (e.g. `typedef cls#(P,Q) this_type;` inside a parameterized class)
// would otherwise be treated as ground truth and cause REFDTYPEs in
// the body to be retargeted at the bare template.
if (AstClass* const ownerClassp = VN_CAST(m_ownerModp, Class)) {
if (ownerClassp->hasGParam()) return;
}
for (AstNode* sp = m_ownerModp->stmtsp(); sp; sp = sp->nextp()) {
AstTypedef* const tdp = VN_CAST(sp, Typedef);
if (!tdp) continue;
AstClassRefDType* const crdtp = VN_CAST(tdp->subDTypep(), ClassRefDType);
if (!crdtp || !crdtp->classp()) continue;
AstClass* const targetp = crdtp->classp();
// A typedef pointing at a still-generic template is a forward
// declaration / unresolved reference, not ground truth.
if (targetp->hasGParam()) continue;
const std::string origName
= targetp->origName().empty() ? targetp->name() : targetp->origName();
const auto pair = m_origNameToClone.emplace(origName, targetp);
// Two locally-resolved typedefs pointing at different
// specializations of the same template -> ambiguous; we cannot
// safely guess which one a stripped REFDTYPE meant.
if (!pair.second && pair.first->second != targetp) m_ambiguous.insert(origName);
}
}
// 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
// being created, so when a referenced parameterized class is cloned more
// than once (e.g. driver -> driver__I5 + driver__I6), the second sibling
// stomps the bindings made by the first. By this point each owner's
// typedefs have been correctly bound (via classRefDeparam), so we use
// those local typedefs as ground truth.
void retargetRefDType(AstRefDType* refp) {
if (!m_ownerModp) 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
= oldOwnerp->origName().empty() ? oldOwnerp->name() : oldOwnerp->origName();
if (m_ambiguous.count(origName)) {
// The owner has two locally-resolved typedefs pointing at
// different specializations of the same template, e.g.:
// typedef driver #(.IW(5)) drv5_t;
// typedef driver #(.IW(6)) drv6_t;
// A stripped REFDTYPE only carries the origName, so we cannot
// tell which sibling it meant; leave it alone.
UINFO(9, "post-param REFDTYPE retarget skipped (ambiguous owner '" << origName
<< "'): " << refp);
return;
}
const auto it = m_origNameToClone.find(origName);
if (it == m_origNameToClone.end()) return;
AstClass* const correctClassp = it->second;
if (correctClassp == oldOwnerp) return;
AstTypedef* const newTdp
= V3LinkDotIfaceCapture::findTypedefInModule(correctClassp, refp->name());
if (!newTdp) {
// The owner map says correctClassp is the right specialization,
// but it does not declare a typedef named refp->name(). Caveats
// of findTypedefInModule (e.g. typedef declared in a base class,
// not the specialization we resolved to) can land us here.
UINFO(9, "post-param REFDTYPE retarget skipped (no typedef '"
<< refp->name() << "' in " << correctClassp->name() << "): " << refp);
return;
}
// typedefp / classOrPackagep / refDTypep must agree. If newTdp lacks
// a subDTypep we cannot keep all three in sync, so skip the entire
// retarget rather than leaving the REFDTYPE half-updated.
if (!newTdp->subDTypep()) {
UINFO(9, "post-param REFDTYPE retarget skipped (newTdp has null subDTypep): " << refp);
return;
}
refp->typedefp(newTdp);
refp->classOrPackagep(correctClassp);
refp->refDTypep(newTdp->subDTypep());
UINFO(9, "post-param REFDTYPE re-resolve: " << refp << " from " << oldOwnerp->name()
<< " to " << correctClassp->name());
}
public:
explicit ParamClassRefDTypeRelinkVisitor(AstNetlist* netlistp) { iterate(netlistp); }
void visit(AstNodeModule* nodep) override {
VL_RESTORER(m_ownerModp);
m_ownerModp = nodep;
iterateChildren(nodep);
}
void visit(AstClass* nodep) override {
VL_RESTORER(m_classp);
VL_RESTORER(m_ownerModp);
m_classp = nodep;
m_ownerModp = nodep;
for (AstNode* stmtp = nodep->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
if (AstParamTypeDType* const ptp = VN_CAST(stmtp, ParamTypeDType)) {
m_paramTypeClassMap[ptp] = nodep;
@ -2317,29 +2426,38 @@ public:
}
void visit(AstRefDType* nodep) override {
iterateChildren(nodep);
AstParamTypeDType* const paramtypep = VN_CAST(nodep->refDTypep(), ParamTypeDType);
if (!paramtypep) return;
const auto it = m_paramTypeClassMap.find(paramtypep);
if (it == m_paramTypeClassMap.end()) return;
AstClass* const origClassp = it->second;
if (!origClassp->hasGParam()) return; // only relink refs to original param classes
if (origClassp->user3p()) return; // will not get removed, no need to relink
AstClass* const parametrizedClassp = VN_CAST(origClassp->user4p(), Class);
if (!parametrizedClassp) return;
const string paramName = paramtypep->name();
AstParamTypeDType* newParamTypep = nullptr;
for (AstNode* stmtp = parametrizedClassp->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
if (AstParamTypeDType* const ptp = VN_CAST(stmtp, ParamTypeDType)) {
if (ptp->name() == paramName) {
newParamTypep = ptp;
break;
// Redirect refs whose refDTypep points at a stale ParamTypeDType in
// a generic class template that has been replaced by a parametrized
// clone.
if (AstParamTypeDType* const paramtypep = VN_CAST(nodep->refDTypep(), ParamTypeDType)) {
const auto it = m_paramTypeClassMap.find(paramtypep);
if (it != m_paramTypeClassMap.end()) {
AstClass* const origClassp = it->second;
if (origClassp->hasGParam() && !origClassp->user3p()) {
if (AstClass* const parametrizedClassp
= VN_CAST(origClassp->user4p(), Class)) {
const string paramName = paramtypep->name();
AstParamTypeDType* newParamTypep = nullptr;
for (AstNode* stmtp = parametrizedClassp->stmtsp(); stmtp;
stmtp = stmtp->nextp()) {
if (AstParamTypeDType* const ptp = VN_CAST(stmtp, ParamTypeDType)) {
if (ptp->name() == paramName) {
newParamTypep = ptp;
break;
}
}
}
if (newParamTypep) {
nodep->refDTypep(newParamTypep);
nodep->classOrPackagep(parametrizedClassp);
}
}
}
}
}
if (newParamTypep) {
nodep->refDTypep(newParamTypep);
nodep->classOrPackagep(parametrizedClassp);
}
// Retarget REFDTYPE.typedefp using the current owner's own
// resolved typedefs as ground truth (handles sibling-clone stomping).
retargetRefDType(nodep);
}
void visit(AstNode* nodep) override { iterateChildren(nodep); }
};

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('vlt')
test.compile(verilator_flags2=["--binary"])
test.execute()
test.passes()

View File

@ -0,0 +1,39 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// Regression test for a name-resolution bug:
// A parameterized module that re-typedefs a typedef out of a parameterized
// class (`typedef cls#(...)::name name;`) used to spuriously fail with
// "Reference to '<name>' before declaration (IEEE 1800-2023 6.18)" when
// the parameterized class had 2+ parameters and the enclosing module had
// any module parameter. Reduced from pulp-platform/axi
// tb_axi_iw_converter.sv:182.
//
// 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 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 rand_master #(parameter int AW = 32, parameter int DW = 32);
typedef logic [AW-1:0] addr_t;
endclass
endpackage
module t #(parameter int unused = 1);
// AW and DW are both overridden to non-default values so that a regression
// in parameter propagation through the typedef chain shows up as a width
// mismatch instead of silently succeeding.
typedef pkg::rand_master #(.AW(17), .DW(64)) rand_master_t;
typedef rand_master_t::addr_t addr_t;
initial begin
static addr_t a = '0;
`checkd($bits(addr_t), 17);
`checkd($bits(a), 17);
$write("*-* All Finished *-*\n");
$finish;
end
endmodule

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('vlt')
test.compile(verilator_flags2=["--binary"])
test.execute()
test.passes()

View File

@ -0,0 +1,72 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// Regression test for a parameterized-class typedef-chain bug.
//
// A parameterized class M re-typedefs a nested type from another
// parameterized class D (M::beat_t = M::driver_t::beat_t). When M and D
// are each instantiated with two distinct parameter values, the type of
// an M::beat_t local must resolve to the matching specialization of beat
// for the enclosing M instantiation. Previously the second sibling clone
// stomped the typedefp binding on the first sibling's REFDTYPE, producing
// a spurious "Function Argument expects ... beat__I5, got ... beat__I6"
// error.
//
// Reduced from axi_test::axi_rand_master usage in pulp-platform/axi
// (tb_axi_xbar with NumMasters>1 or NumSlaves>1).
//
// 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 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 beat #(parameter int IW = 8);
logic [IW-1:0] id;
endclass
class driver #(parameter int IW = 8);
typedef beat #(.IW(IW)) beat_t;
// Verify the beat handed to us has the IW we were specialized for.
task send(input beat_t b);
`checkd($bits(b.id), IW);
`checkd(b.id, IW'(IW));
endtask
endclass
class master #(parameter int IW = 8);
typedef driver #(.IW(IW)) driver_t;
typedef driver_t::beat_t beat_t;
driver_t drv;
function new(driver_t d); drv = d; endfunction
task run();
automatic beat_t b = new;
// The width of beat_t.id must follow this master's IW. If the
// typedefp on the M::beat_t REFDTYPE is stomped by a sibling clone,
// $bits here will not match IW (or compilation will fail outright).
`checkd($bits(b.id), IW);
b.id = IW'(IW);
drv.send(b);
endtask
endclass
endpackage
module t;
pkg::master #(.IW(5)) ma;
pkg::master #(.IW(6)) mb;
pkg::driver #(.IW(5)) da;
pkg::driver #(.IW(6)) db;
initial begin
da = new; ma = new(da); ma.run();
db = new; mb = new(db); mb.run();
$write("*-* All Finished *-*\n");
$finish;
end
endmodule