Fix class::localparam during elaboration (#7524) (#7534)

This commit is contained in:
em2machine 2026-05-06 11:46:33 -04:00 committed by GitHub
parent 252afef499
commit d5f9385e9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 273 additions and 3 deletions

View File

@ -1278,6 +1278,8 @@ class AstNetlist final : public AstNode {
VTimescale m_timeprecision; // Global time precision
bool m_timescaleSpecified = false; // Input HDL specified timescale
uint32_t m_nTraceCodes = 0; // Number of trace codes used by design
// V3Param-deferred params awaiting V3LinkDot::linkDotParamed scope-resolution.
std::vector<AstVar*> m_deferredParamVarps;
// Sparse metadata for constants produced from named parameters/localparams. Keep this off
// AstConst itself, as AstConst is a very common node and only a small fraction carry this
// name.
@ -1286,6 +1288,10 @@ class AstNetlist final : public AstNode {
public:
AstNetlist();
ASTGEN_MEMBERS_AstNetlist;
const char* broken() const override;
void pushDeferredParamVarp(AstVar* varp) { m_deferredParamVarps.push_back(varp); }
const std::vector<AstVar*>& deferredParamVarps() const { return m_deferredParamVarps; }
void clearDeferredParamVarps() { m_deferredParamVarps.clear(); }
void deleteContents();
void cloneRelink() override { V3ERROR_NA; } // Not cloneable
string name() const override VL_MT_STABLE { return "$root"; }

View File

@ -543,6 +543,13 @@ AstNetlist::AstNetlist()
addMiscsp(m_constPoolp);
}
const char* AstNetlist::broken() const {
for (const AstVar* const varp : m_deferredParamVarps) {
BROKEN_RTN(!varp || !varp->brokeExists());
}
return nullptr;
}
string AstNetlist::astConstOrigParamName(const AstConst* nodep) const {
if (!nodep->num().hasOrigParamName()) return "";
const auto it = m_constOrigParamNames.find(nodep);

View File

@ -72,6 +72,13 @@
VL_DEFINE_DEBUG_FUNCTIONS;
// Walk a class-typed node through typedef/RefDType chains to its ClassRefDType, or null.
static AstClassRefDType* classRefDTypeOfNode(AstNode* nodep) {
while (AstTypedef* const tdp = VN_CAST(nodep, Typedef)) nodep = tdp->subDTypep();
AstNodeDType* const dtp = VN_CAST(nodep, NodeDType);
return dtp ? VN_CAST(dtp->skipRefOrNullp(), ClassRefDType) : nullptr;
}
//######################################################################
// Hierarchical block and parameter db (modules without parameters are also handled)
@ -1260,15 +1267,33 @@ class ParamProcessor final {
}
}
}
// Typedef-aliased paramed class (e.g., typedef C#(7) my_c; my_c::b).
if (AstClassRefDType* const crp = classRefDTypeOfNode(classRefp->classOrPackageNodep())) {
if (AstClass* const srcClassp = crp->classp()) {
if (srcClassp->hasGParam() && crp->paramsp()) {
classRefDeparam(crp, srcClassp);
if (AstClass* const newClassp = crp->classp()) lhsClassp = newClassp;
}
}
}
if (!lhsClassp) return;
AstTypedef* const tdefp
= VN_CAST(m_memberMap.findMember(lhsClassp, parseRefp->name()), Typedef);
if (tdefp) {
AstNode* const memberp = m_memberMap.findMember(lhsClassp, parseRefp->name());
if (AstTypedef* const tdefp = VN_CAST(memberp, Typedef)) {
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);
}
}
}
}
@ -2167,6 +2192,14 @@ 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);
// Evaluate all module constants
V3Const::constifyParamsEdit(nodep);
// Set name for warnings for when we param propagate the module
@ -2756,6 +2789,13 @@ class ParamVisitor final : public VNVisitor {
}
});
if (hasUnresolvedLparamXRef) return;
// Defer if value contains a class::member Dot. By V3Param time the only
// surviving such Dots are paramed-class refs awaiting linkDotParamed.
if (nodep->valuep()->exists(
[](AstDot* dotp) { return VN_IS(dotp->lhsp(), ClassOrPackageRef); })) {
v3Global.rootp()->pushDeferredParamVarp(nodep);
return;
}
V3Const::constifyParamsEdit(nodep);
}
}
@ -3254,6 +3294,7 @@ public:
void V3Param::param(AstNetlist* rootp) {
UINFO(2, __FUNCTION__ << ":");
rootp->clearDeferredParamVarps(); // Defensive: drop any stragglers from a prior invocation
if (dumpTreeEitherLevel() >= 9) V3LinkDotIfaceCapture::dumpEntries("before V3Param");
{ ParamTop{rootp}; }
@ -3262,3 +3303,11 @@ void V3Param::param(AstNetlist* rootp) {
V3Global::dumpCheckGlobalTree("param", 0, dumpTreeEitherLevel() >= 3);
}
void V3Param::finalizeDeferredParams(AstNetlist* rootp) {
// Constify params whose Dot RHS was resolved by V3LinkDot::linkDotParamed.
for (AstVar* const varp : rootp->deferredParamVarps()) {
if (varp->valuep() && !VN_IS(varp->valuep(), Const)) V3Const::constifyParamsEdit(varp);
}
rootp->clearDeferredParamVarps();
}

View File

@ -27,6 +27,7 @@ class AstNetlist;
class V3Param final {
public:
static void param(AstNetlist* rootp) VL_MT_DISABLED;
static void finalizeDeferredParams(AstNetlist* rootp) VL_MT_DISABLED;
};
#endif // Guard

View File

@ -183,6 +183,7 @@ static void process() {
V3Param::param(v3Global.rootp());
V3LinkDot::linkDotParamed(v3Global.rootp()); // Cleanup as made new modules
V3Param::finalizeDeferredParams(v3Global.rootp());
V3LinkLValue::linkLValue(v3Global.rootp()); // Resolve new VarRefs
// Link cleanup of 'with' as final link phase before V3Width

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()
test.execute()
test.passes()

View File

@ -0,0 +1,96 @@
// 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
// Tests reference into a parameterized class via :: in a parameter expression.
virtual class C #(parameter int a = 0);
localparam int b = a;
static function int get_a();
return a;
endfunction
typedef enum {E0 = 100, E1 = 101} e_t;
endclass
class D #(parameter int v = C#(7)::b);
static function int get_v();
return v;
endfunction
endclass
typedef C#(0) inst0;
typedef C#(1) inst1;
typedef C#(4) chain_a;
typedef chain_a chain_b;
module t (/*AUTOARG*/);
// Wilson's exact case: typedef-aliased paramed class lparam.
localparam int LP_TYPEDEF_LPARAM = inst0::b;
// Direct (no typedef) paramed class lparam.
localparam int LP_DIRECT_LPARAM = C#(2)::b;
// Static method RHS via typedef.
localparam int LP_STATIC_FUNC = inst1::get_a();
// Enum value RHS, direct and typedef-aliased.
localparam int LP_ENUM_DIRECT = int'(C#(3)::E0);
localparam int LP_ENUM_TYPEDEF = int'(inst0::E1);
// Override propagation: explicit value should reach the lparam.
localparam int LP_OVERRIDE = C#(7)::a;
// Compile-time math inside the class parameterization.
localparam int LP_MATH = C#(2 + 3)::a;
// Module localparam used as the class parameter.
localparam int P = 5;
localparam int LP_PARAM_DRIVES_PARAM = C#(P)::a;
// Chained typedef alias: typedef A B; B::b should resolve through both.
localparam int LP_CHAIN_TYPEDEF = chain_b::b;
// Multiple Dots in one expression with different specializations.
localparam int LP_MULTI = C#(0)::a + C#(1)::a + C#(2)::a;
// Class param default itself uses a paramed-class Dot, then we read v.
localparam int LP_CLASS_DEFAULT = D#()::get_v();
// Nested: paramed-class Dot used as the explicit parameter to another paramed class.
localparam int LP_NESTED_ARG = D#(C#(9)::b)::get_v();
// Typedef-aliased paramed class as inner of nested arg (silent-miscompile risk).
typedef C#(13) c13;
localparam int LP_NESTED_TYPEDEF_INNER = D#(c13::b)::get_v();
// 3-level nesting via lparam access: D's v gets D#(C#(15)::b), then read v.
localparam int LP_THREE_LEVEL = D#(D#(C#(15)::b)::v)::get_v();
// Chained typedef alias of a typedef of a paramed class.
typedef c13 c13_alias;
localparam int LP_CHAINED_TYPEDEF_INNER = D#(c13_alias::b)::get_v();
initial begin
if (LP_TYPEDEF_LPARAM !== 0) begin $write("%%Error: TYPEDEF_LPARAM=%0d\n", LP_TYPEDEF_LPARAM); $stop; end
if (LP_DIRECT_LPARAM !== 2) begin $write("%%Error: DIRECT_LPARAM=%0d\n", LP_DIRECT_LPARAM); $stop; end
if (LP_STATIC_FUNC !== 1) begin $write("%%Error: STATIC_FUNC=%0d\n", LP_STATIC_FUNC); $stop; end
if (LP_ENUM_DIRECT !== 100) begin $write("%%Error: ENUM_DIRECT=%0d\n", LP_ENUM_DIRECT); $stop; end
if (LP_ENUM_TYPEDEF !== 101) begin $write("%%Error: ENUM_TYPEDEF=%0d\n", LP_ENUM_TYPEDEF); $stop; end
if (LP_OVERRIDE !== 7) begin $write("%%Error: OVERRIDE=%0d\n", LP_OVERRIDE); $stop; end
if (LP_MATH !== 5) begin $write("%%Error: MATH=%0d\n", LP_MATH); $stop; end
if (LP_PARAM_DRIVES_PARAM !== 5) begin $write("%%Error: PARAM_DRIVES_PARAM=%0d\n", LP_PARAM_DRIVES_PARAM); $stop; end
if (LP_CHAIN_TYPEDEF !== 4) begin $write("%%Error: CHAIN_TYPEDEF=%0d\n", LP_CHAIN_TYPEDEF); $stop; end
if (LP_MULTI !== 3) begin $write("%%Error: MULTI=%0d\n", LP_MULTI); $stop; end
if (LP_CLASS_DEFAULT !== 7) begin $write("%%Error: CLASS_DEFAULT=%0d\n", LP_CLASS_DEFAULT); $stop; end
if (LP_NESTED_ARG !== 9) begin $write("%%Error: NESTED_ARG=%0d\n", LP_NESTED_ARG); $stop; end
if (LP_NESTED_TYPEDEF_INNER !== 13) begin $write("%%Error: NESTED_TYPEDEF_INNER=%0d\n", LP_NESTED_TYPEDEF_INNER); $stop; end
if (LP_THREE_LEVEL !== 15) begin $write("%%Error: THREE_LEVEL=%0d\n", LP_THREE_LEVEL); $stop; end
if (LP_CHAINED_TYPEDEF_INNER !== 13) begin $write("%%Error: CHAINED_TYPEDEF_INNER=%0d\n", LP_CHAINED_TYPEDEF_INNER); $stop; end
$write("*-* All Finished *-*\n");
$finish;
end
endmodule

View File

@ -0,0 +1,6 @@
%Error: t/t_class_localparam_dotref_bad.v:17:33: Can't find definition of scope/variable/func: 'nonexistent'
: ... note: In instance 't'
17 | localparam int LP_BAD = inst::nonexistent;
| ^~~~~~~~~~~
... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance.
%Error: Exiting due to

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()

View File

@ -0,0 +1,18 @@
// 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
// Negative test: a parameterized class :: ref to a member that does not exist
// in a parameter expression should produce a clean error, not crash.
class C #(parameter int a = 0);
localparam int b = a;
endclass
typedef C#(0) inst;
module t;
localparam int LP_BAD = inst::nonexistent;
endmodule

View File

@ -0,0 +1,12 @@
%Error-UNSUPPORTED: t/t_class_localparam_dotref_funcref_arg_bad.v:23:32: dotted expressions in parameters
: ... note: In instance 't'
: ... Suggest use a typedef
23 | localparam int y = D#(C#(7)::get_a())::x;
| ^~~~~
... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest
%Error: t/t_class_localparam_dotref_funcref_arg_bad.v:23:32: Can't convert defparam value to constant: Param '__paramNumber1' of 'D'
: ... note: In instance 't'
23 | localparam int y = D#(C#(7)::get_a())::x;
| ^~~~~
... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance.
%Error: Exiting due to

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()

View File

@ -0,0 +1,24 @@
// 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
// Negative test: a paramed-class static function call used directly as the
// parameter argument to another paramed class is not currently supported.
// Folding the function call to a constant during specialization requires
// constant-function evaluation that this fix does not provide.
class C #(parameter int a = 0);
static function int get_a();
return a;
endfunction
endclass
class D #(parameter int v = 0);
localparam int x = v;
endclass
module t;
localparam int y = D#(C#(7)::get_a())::x;
endmodule