From 7a9563db57ef2c3f46e5b82c5a92ef26556e49b7 Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Tue, 9 Jun 2026 11:28:39 -0400 Subject: [PATCH 01/17] more tests and fixes --- src/V3Param.cpp | 64 ++++++++++++++--- .../t/t_class_lparam_generate_chain.py | 18 +++++ .../t/t_class_lparam_generate_chain.v | 65 +++++++++++++++++ test_regress/t/t_class_lparam_iface_chain.py | 18 +++++ test_regress/t/t_class_lparam_iface_chain.v | 66 +++++++++++++++++ test_regress/t/t_class_lparam_module_chain.py | 18 +++++ test_regress/t/t_class_lparam_module_chain.v | 71 +++++++++++++++++++ .../t/t_class_lparam_typedef_chain.py | 18 +++++ test_regress/t/t_class_lparam_typedef_chain.v | 60 ++++++++++++++++ 9 files changed, 390 insertions(+), 8 deletions(-) create mode 100755 test_regress/t/t_class_lparam_generate_chain.py create mode 100644 test_regress/t/t_class_lparam_generate_chain.v create mode 100755 test_regress/t/t_class_lparam_iface_chain.py create mode 100644 test_regress/t/t_class_lparam_iface_chain.v create mode 100755 test_regress/t/t_class_lparam_module_chain.py create mode 100644 test_regress/t/t_class_lparam_module_chain.v create mode 100755 test_regress/t/t_class_lparam_typedef_chain.py create mode 100644 test_regress/t/t_class_lparam_typedef_chain.v diff --git a/src/V3Param.cpp b/src/V3Param.cpp index 063c55d3a..fc584e499 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -2178,6 +2178,50 @@ public: }); } + // Resolve `class::member` Dots in `rootp`'s tree, and also in any deferred + // lparam reachable transitively through VarRefs from `rootp`. By V3Param + // time the surviving such Dots are paramed-class refs that need + // linkDotParamed for scope, but `resolveDotToTypedef` can handle the + // typedef-aliased subset inline. Resolving them here turns a deferred + // lparam's value into a Const so subsequent constify on `rootp` can fold + // expressions that reference the lparam. + // `modp` is the enclosing module — resolveDotToTypedef may call into + // classRefDeparam which needs m_modp set for level/recursion bookkeeping. + void resolveDeferredDotsReachableFrom(AstNode* rootp, AstNodeModule* modp) { + std::vector dotps; + const auto& deferredVarps = v3Global.rootp()->deferredParamVarps(); + std::set reachedDeferred; + auto collectDots = [&](AstNode* p) { + p->foreach([&](AstDot* dotp) { + if (VN_IS(dotp->lhsp(), ClassOrPackageRef)) dotps.push_back(dotp); + }); + }; + std::vector workVarps; + auto enqueueRefs = [&](AstNode* p) { + p->foreach([&](const AstVarRef* refp) { + AstVar* const refVarp = refp->varp(); + if (refVarp && refVarp->varType() == VVarType::LPARAM + && deferredVarps.count(refVarp) && reachedDeferred.insert(refVarp).second) { + workVarps.push_back(refVarp); + } + }); + }; + collectDots(rootp); + enqueueRefs(rootp); + while (!workVarps.empty()) { + AstVar* const varp = workVarps.back(); + workVarps.pop_back(); + if (!varp->valuep()) continue; + collectDots(varp->valuep()); + enqueueRefs(varp->valuep()); + } + if (dotps.empty()) return; + VL_RESTORER(m_modp); + m_modp = modp; + // Reverse-iterate so inner Dots resolve before outer. + for (auto it = dotps.rbegin(); it != dotps.rend(); ++it) resolveDotToTypedef(*it); + } + AstNodeModule* nodeDeparam(AstNode* nodep, AstNodeModule* srcModp, AstNodeModule* modp, const string& someInstanceName) { // Return new or reused de-parameterized module @@ -2218,14 +2262,9 @@ public: } // Create new module name with _'s between the constants UINFOTREE(10, nodep, "", "cell"); - // Resolve `class::member` Dots in pin values so constify sees Consts. - // Single-pass: filter-collect class-scoped Dots, then reverse-iterate so inner - // Dots resolve before outer (vector stays empty for cells with no class Dots). - std::vector dotps; - nodep->foreach([&](AstDot* dotp) { - if (VN_IS(dotp->lhsp(), ClassOrPackageRef)) dotps.push_back(dotp); - }); - for (auto it = dotps.rbegin(); it != dotps.rend(); ++it) resolveDotToTypedef(*it); + // Resolve `class::member` Dots in pin values, and in any deferred + // lparam reachable from the pin tree, so constify sees Consts. + resolveDeferredDotsReachableFrom(nodep, modp); // Evaluate all module constants V3Const::constifyParamsEdit(nodep); // Set name for warnings for when we param propagate the module @@ -3173,6 +3212,9 @@ class ParamVisitor final : public VNVisitor { void visit(AstGenIf* nodep) override { UINFO(9, " GENIF " << nodep); iterateAndNextNull(nodep->condp()); + // condp may reference deferred lparams whose Dot is still pending; + // resolve them so widthing/constify can see Consts. + m_processor.resolveDeferredDotsReachableFrom(nodep->condp(), m_modp); // 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. @@ -3208,6 +3250,9 @@ class ParamVisitor final : public VNVisitor { iterateAndNextNull(forp->initsp()); iterateAndNextNull(forp->condp()); iterateAndNextNull(forp->incsp()); + // Cond/init/inc may reference deferred lparams whose Dot is still + // pending; resolve them so widthing/constify can see Consts. + m_processor.resolveDeferredDotsReachableFrom(forp, m_modp); 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 @@ -3241,6 +3286,9 @@ class ParamVisitor final : public VNVisitor { AstNode* keepp = nullptr; iterateAndNextNull(nodep->exprp()); V3Case::caseLint(nodep); + // expr / case items may reference deferred lparams whose Dot is still + // pending; resolve them so widthing/constify can see Consts. + m_processor.resolveDeferredDotsReachableFrom(nodep, 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 diff --git a/test_regress/t/t_class_lparam_generate_chain.py b/test_regress/t/t_class_lparam_generate_chain.py new file mode 100755 index 000000000..33b3f6c52 --- /dev/null +++ b/test_regress/t/t_class_lparam_generate_chain.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Edmund Lam +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--binary']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_class_lparam_generate_chain.v b/test_regress/t/t_class_lparam_generate_chain.v new file mode 100644 index 000000000..9b2074178 --- /dev/null +++ b/test_regress/t/t_class_lparam_generate_chain.v @@ -0,0 +1,65 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Generate-for / generate-if / generate-case conditions that reference +// a class-scope-resolved deferred localparam (e.g. inst::b). V3Param's +// visit(AstGenIf/Block/Case) calls V3Width::widthParamsEdit / +// constify on the cond/expr; the same transitive Dot-resolution used +// for cell pins must be applied here so the deferred lparam folds. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Edmund Lam +// 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 + +// Cell whose presence is observable, used inside generate blocks +// below to confirm each generate flavour elaborated the right way. +module Tag #( + parameter int ID +) (); + initial $write("Tag ID=%0d\n", ID); +endmodule + +module t; + virtual class C #( + parameter int a + ); + localparam int b = a; + endclass + + typedef C#(3) c3; + typedef C#(5) c5; + + // Deferred lparams + localparam int b3 = c3::b; + localparam int b5 = c5::b; + + // (1) Generate-for bound = deferred lparam + for (genvar i = 0; i < b3; i++) begin : gf + Tag #(100 + i) inst (); + end + + // (2) Generate-if cond = deferred lparam + if (b5 > b3) begin : gi_t + Tag #(200) inst (); + end else begin : gi_f + Tag #(201) inst (); + end + + // (3) Generate-case selector = deferred lparam + case (b5) + 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(b3, 32'd3); + `checkh(b5, 32'd5); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_class_lparam_iface_chain.py b/test_regress/t/t_class_lparam_iface_chain.py new file mode 100755 index 000000000..33b3f6c52 --- /dev/null +++ b/test_regress/t/t_class_lparam_iface_chain.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Edmund Lam +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--binary']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_class_lparam_iface_chain.v b/test_regress/t/t_class_lparam_iface_chain.v new file mode 100644 index 000000000..0fb5b8ff7 --- /dev/null +++ b/test_regress/t/t_class_lparam_iface_chain.v @@ -0,0 +1,66 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Interface-instance parameter pins whose value chains through a +// class-scope-resolved localparam (typedef alias of a parameterized +// class, e.g. inst::b). Same cell-deparam fix that handles module +// instances must also handle interface instances and downstream +// port-bound consumers. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Edmund Lam +// 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 + +interface SubIface #( + parameter int IW +) (); + logic [IW-1:0] data; +endinterface + +// Module that takes an interface port — must elaborate after the iface +// cell pin is fully constified. +module Consumer ( + SubIface si +); +endmodule + +module t; + virtual class C #( + parameter int a + ); + localparam int b = a; + endclass + + typedef C#(8) c8; + typedef C#(13) c13; + + // Deferred lparams + localparam int b8 = c8::b; + localparam int b13 = c13::b; + // Chained + localparam int b8_alias = b8; + + // Iface pin = bare VarRef to deferred lparam + SubIface #(b8) i_bare (); + // Iface pin = chained VarRef + SubIface #(b8_alias) i_chain (); + // Iface pin = expression mixing deferred lparams + SubIface #(b8 + b13) i_expr (); + // Iface used as a module port — exercises the iface-cell-first path + Consumer cons (.si(i_bare)); + + initial begin + `checkh(b8, 32'd8); + `checkh(b13, 32'd13); + `checkh(b8_alias, 32'd8); + `checkh($bits(i_bare.data), 32'd8); + `checkh($bits(i_chain.data), 32'd8); + `checkh($bits(i_expr.data), 32'd21); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_class_lparam_module_chain.py b/test_regress/t/t_class_lparam_module_chain.py new file mode 100755 index 000000000..33b3f6c52 --- /dev/null +++ b/test_regress/t/t_class_lparam_module_chain.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Edmund Lam +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--binary']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_class_lparam_module_chain.v b/test_regress/t/t_class_lparam_module_chain.v new file mode 100644 index 000000000..c2d6667e8 --- /dev/null +++ b/test_regress/t/t_class_lparam_module_chain.v @@ -0,0 +1,71 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Module-instance parameter pins whose value chains through a +// class-scope-resolved localparam (typedef alias of a parameterized +// class, e.g. inst::b). V3LinkDot defers the inst::b Dot until +// post-V3Param; the cell deparam must follow VarRefs into any +// deferred lparam reachable from the pin tree and resolve its Dots +// inline so constify on the cell can fold. +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Edmund Lam +// 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 + +module Sub #( + parameter int WIDTH +) (); +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 +) (); + Sub #(W) inner (); +endmodule + +module t; + virtual class C #( + parameter int a + ); + localparam int b = a; + endclass + + typedef C#(4) c4; + typedef C#(5) c5; + typedef C#(8) c8; + + // Deferred lparams (value contains class::member Dot) + localparam int b4 = c4::b; + localparam int b5 = c5::b; + localparam int b8 = c8::b; + // Chained: value references another deferred lparam + localparam int c8_ref = b8; + localparam int d8_ref = c8_ref + 1; + + // Pin is bare VarRef to a deferred lparam + Sub #(b8) m_bare (); + // Pin chains through multiple deferred lparams (b8 -> c8_ref -> d8_ref) + Sub #(d8_ref) m_chain (); + // Pin is an expression mixing two deferred lparams + Sub #(b4 + b5) m_expr (); + // Pin mixes a Dot and a deferred lparam in the same expression + Sub #(c4::b + b5) m_mix (); + // Two-level: outer module forwards param to inner cell + SubL1 #(b8) m_l1 (); + + initial begin + `checkh(b4, 32'd4); + `checkh(b5, 32'd5); + `checkh(b8, 32'd8); + `checkh(c8_ref, 32'd8); + `checkh(d8_ref, 32'd9); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_class_lparam_typedef_chain.py b/test_regress/t/t_class_lparam_typedef_chain.py new file mode 100755 index 000000000..33b3f6c52 --- /dev/null +++ b/test_regress/t/t_class_lparam_typedef_chain.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Edmund Lam +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--binary']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_class_lparam_typedef_chain.v b/test_regress/t/t_class_lparam_typedef_chain.v new file mode 100644 index 000000000..98ed4457a --- /dev/null +++ b/test_regress/t/t_class_lparam_typedef_chain.v @@ -0,0 +1,60 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Typedefs and class type parameters that resolve through a +// class-scope-resolved localparam (typedef alias of a parameterized +// class, e.g. inst::b). Exercises three sub-cases: +// - Deferred lparam used as a packed range bound in a typedef +// - Deferred lparam used as a value argument to a parameterized class +// - Class-scope-resolved typedef that itself depends on the param +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Edmund Lam +// 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 + +module t; + virtual class C #( + parameter int a + ); + localparam int b = a; + typedef logic [a-1:0] inner_t; + // localparam derived from a typedef inside the same class — + // resolveDotToTypedef must constify this when reached via CFG::width. + localparam int width = $bits(inner_t); + endclass + + typedef C#(8) c8; + + // Deferred lparam (direct Dot) + localparam int b8 = c8::b; + + // (1) typedef range driven by a deferred lparam + typedef logic [b8-1:0] data_t; + data_t data_value; + + // (2) class type-arg uses a deferred lparam value + typedef C#(b8) c_from_def; + localparam int from_def_b = c_from_def::b; + + // (3) class-scope-resolved typedef computed via $bits(inner_t) + localparam int via_bits = c8::width; + + // Wire whose range comes from a deferred lparam + logic [b8-1:0] wide_bus; + + initial begin + `checkh(b8, 32'd8); + `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); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule From d33de9680a3ae5d2c373572362b04e1658b67f95 Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Tue, 9 Jun 2026 11:32:06 -0400 Subject: [PATCH 02/17] add name to contributors --- docs/CONTRIBUTORS | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/CONTRIBUTORS b/docs/CONTRIBUTORS index 880b5608a..dc5ef62b5 100644 --- a/docs/CONTRIBUTORS +++ b/docs/CONTRIBUTORS @@ -65,6 +65,7 @@ Drew Ranck Drew Taussig Driss Hafdi Edgar E. Iglesias +Edmund Lam Eric Mejdrich Eric Müller Eric Rippey From d5fd2e468f398106963db6faa6be87d0cf5daf3d Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Tue, 9 Jun 2026 11:53:49 -0400 Subject: [PATCH 03/17] cleanup --- src/V3Param.cpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/V3Param.cpp b/src/V3Param.cpp index fc584e499..76422a89d 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -2178,15 +2178,8 @@ public: }); } - // Resolve `class::member` Dots in `rootp`'s tree, and also in any deferred - // lparam reachable transitively through VarRefs from `rootp`. By V3Param - // time the surviving such Dots are paramed-class refs that need - // linkDotParamed for scope, but `resolveDotToTypedef` can handle the - // typedef-aliased subset inline. Resolving them here turns a deferred - // lparam's value into a Const so subsequent constify on `rootp` can fold - // expressions that reference the lparam. - // `modp` is the enclosing module — resolveDotToTypedef may call into - // classRefDeparam which needs m_modp set for level/recursion bookkeeping. + // Resolve `class::member` Dots depended on by `rootp`'s tree, and also recursively through + // VarRefs to all leaf dependencies. Then resolve these to typedefs. void resolveDeferredDotsReachableFrom(AstNode* rootp, AstNodeModule* modp) { std::vector dotps; const auto& deferredVarps = v3Global.rootp()->deferredParamVarps(); @@ -2208,6 +2201,7 @@ public: }; collectDots(rootp); enqueueRefs(rootp); + // Handle deferred VarRefs recursively until no more, accumulating Dots along the way. while (!workVarps.empty()) { AstVar* const varp = workVarps.back(); workVarps.pop_back(); From e0c55cc29667e20697aa7bbf165bcc66fa959c91 Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Tue, 9 Jun 2026 12:43:43 -0400 Subject: [PATCH 04/17] fix copyright text --- test_regress/t/t_class_lparam_generate_chain.py | 2 +- test_regress/t/t_class_lparam_generate_chain.v | 2 +- test_regress/t/t_class_lparam_iface_chain.py | 2 +- test_regress/t/t_class_lparam_iface_chain.v | 2 +- test_regress/t/t_class_lparam_module_chain.py | 2 +- test_regress/t/t_class_lparam_module_chain.v | 2 +- test_regress/t/t_class_lparam_typedef_chain.py | 2 +- test_regress/t/t_class_lparam_typedef_chain.v | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test_regress/t/t_class_lparam_generate_chain.py b/test_regress/t/t_class_lparam_generate_chain.py index 33b3f6c52..46d1fe4c0 100755 --- a/test_regress/t/t_class_lparam_generate_chain.py +++ b/test_regress/t/t_class_lparam_generate_chain.py @@ -4,7 +4,7 @@ # 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 Edmund Lam +# SPDX-FileCopyrightText: 2026 Wilson Snyder # SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 import vltest_bootstrap diff --git a/test_regress/t/t_class_lparam_generate_chain.v b/test_regress/t/t_class_lparam_generate_chain.v index 9b2074178..00f9ed8dd 100644 --- a/test_regress/t/t_class_lparam_generate_chain.v +++ b/test_regress/t/t_class_lparam_generate_chain.v @@ -7,7 +7,7 @@ // for cell pins must be applied here so the deferred lparam folds. // // This file ONLY is placed under the Creative Commons Public Domain. -// SPDX-FileCopyrightText: 2026 Edmund Lam +// SPDX-FileCopyrightText: 2026 Wilson Snyder // SPDX-License-Identifier: CC0-1.0 // verilog_format: off diff --git a/test_regress/t/t_class_lparam_iface_chain.py b/test_regress/t/t_class_lparam_iface_chain.py index 33b3f6c52..46d1fe4c0 100755 --- a/test_regress/t/t_class_lparam_iface_chain.py +++ b/test_regress/t/t_class_lparam_iface_chain.py @@ -4,7 +4,7 @@ # 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 Edmund Lam +# SPDX-FileCopyrightText: 2026 Wilson Snyder # SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 import vltest_bootstrap diff --git a/test_regress/t/t_class_lparam_iface_chain.v b/test_regress/t/t_class_lparam_iface_chain.v index 0fb5b8ff7..39444fbf5 100644 --- a/test_regress/t/t_class_lparam_iface_chain.v +++ b/test_regress/t/t_class_lparam_iface_chain.v @@ -7,7 +7,7 @@ // port-bound consumers. // // This file ONLY is placed under the Creative Commons Public Domain. -// SPDX-FileCopyrightText: 2026 Edmund Lam +// SPDX-FileCopyrightText: 2026 Wilson Snyder // SPDX-License-Identifier: CC0-1.0 // verilog_format: off diff --git a/test_regress/t/t_class_lparam_module_chain.py b/test_regress/t/t_class_lparam_module_chain.py index 33b3f6c52..46d1fe4c0 100755 --- a/test_regress/t/t_class_lparam_module_chain.py +++ b/test_regress/t/t_class_lparam_module_chain.py @@ -4,7 +4,7 @@ # 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 Edmund Lam +# SPDX-FileCopyrightText: 2026 Wilson Snyder # SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 import vltest_bootstrap diff --git a/test_regress/t/t_class_lparam_module_chain.v b/test_regress/t/t_class_lparam_module_chain.v index c2d6667e8..09af9308e 100644 --- a/test_regress/t/t_class_lparam_module_chain.v +++ b/test_regress/t/t_class_lparam_module_chain.v @@ -8,7 +8,7 @@ // inline so constify on the cell can fold. // // This file ONLY is placed under the Creative Commons Public Domain. -// SPDX-FileCopyrightText: 2026 Edmund Lam +// SPDX-FileCopyrightText: 2026 Wilson Snyder // SPDX-License-Identifier: CC0-1.0 // verilog_format: off diff --git a/test_regress/t/t_class_lparam_typedef_chain.py b/test_regress/t/t_class_lparam_typedef_chain.py index 33b3f6c52..46d1fe4c0 100755 --- a/test_regress/t/t_class_lparam_typedef_chain.py +++ b/test_regress/t/t_class_lparam_typedef_chain.py @@ -4,7 +4,7 @@ # 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 Edmund Lam +# SPDX-FileCopyrightText: 2026 Wilson Snyder # SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 import vltest_bootstrap diff --git a/test_regress/t/t_class_lparam_typedef_chain.v b/test_regress/t/t_class_lparam_typedef_chain.v index 98ed4457a..c8ec879f8 100644 --- a/test_regress/t/t_class_lparam_typedef_chain.v +++ b/test_regress/t/t_class_lparam_typedef_chain.v @@ -8,7 +8,7 @@ // - Class-scope-resolved typedef that itself depends on the param // // This file ONLY is placed under the Creative Commons Public Domain. -// SPDX-FileCopyrightText: 2026 Edmund Lam +// SPDX-FileCopyrightText: 2026 Wilson Snyder // SPDX-License-Identifier: CC0-1.0 // verilog_format: off From f18aaeea8375b93788fa2a9aebd44d22ed83f9c7 Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Tue, 9 Jun 2026 13:03:00 -0400 Subject: [PATCH 05/17] fix typedefs --- src/V3Param.cpp | 33 ++++++++- test_regress/t/t_class_lparam_nested_chain.py | 18 +++++ test_regress/t/t_class_lparam_nested_chain.v | 72 +++++++++++++++++++ .../t/t_class_lparam_struct_member_chain.py | 18 +++++ .../t/t_class_lparam_struct_member_chain.v | 54 ++++++++++++++ 5 files changed, 192 insertions(+), 3 deletions(-) create mode 100755 test_regress/t/t_class_lparam_nested_chain.py create mode 100644 test_regress/t/t_class_lparam_nested_chain.v create mode 100644 test_regress/t/t_class_lparam_struct_member_chain.py create mode 100644 test_regress/t/t_class_lparam_struct_member_chain.v diff --git a/src/V3Param.cpp b/src/V3Param.cpp index 76422a89d..ea60bd8f1 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -1288,7 +1288,19 @@ class ParamProcessor final { // 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 (!VN_IS(varp->valuep(), Const)) { + // If the value contains nested class::member Dots (e.g. this + // member's expression references another paramed class), resolve + // those first so constify can fold the whole chain. + std::vector nestedDots; + varp->valuep()->foreach([&](AstDot* dp) { + if (VN_IS(dp->lhsp(), ClassOrPackageRef)) nestedDots.push_back(dp); + }); + for (auto it = nestedDots.rbegin(); it != nestedDots.rend(); ++it) { + resolveDotToTypedef(*it); + } + V3Const::constifyParamsEdit(varp); + } if (AstConst* const constp = VN_CAST(varp->valuep(), Const)) { dotp->replaceWith(constp->cloneTree(false)); VL_DO_DANGLING(dotp->deleteTree(), dotp); @@ -2182,13 +2194,21 @@ public: // VarRefs to all leaf dependencies. Then resolve these to typedefs. void resolveDeferredDotsReachableFrom(AstNode* rootp, AstNodeModule* modp) { std::vector dotps; + std::vector tdefps; const auto& deferredVarps = v3Global.rootp()->deferredParamVarps(); std::set reachedDeferred; + std::set reachedTypedefs; auto collectDots = [&](AstNode* p) { p->foreach([&](AstDot* dotp) { if (VN_IS(dotp->lhsp(), ClassOrPackageRef)) dotps.push_back(dotp); }); }; + auto collectTypedefs = [&](AstNode* p) { + p->foreach([&](const AstRefDType* refp) { + AstTypedef* const tdefp = refp->typedefp(); + if (tdefp && reachedTypedefs.insert(tdefp).second) tdefps.push_back(tdefp); + }); + }; std::vector workVarps; auto enqueueRefs = [&](AstNode* p) { p->foreach([&](const AstVarRef* refp) { @@ -2200,18 +2220,25 @@ public: }); }; collectDots(rootp); + collectTypedefs(rootp); enqueueRefs(rootp); - // Handle deferred VarRefs recursively until no more, accumulating Dots along the way. + // Handle deferred VarRefs recursively until no more, accumulating Dots and + // typedef references along the way. while (!workVarps.empty()) { AstVar* const varp = workVarps.back(); workVarps.pop_back(); if (!varp->valuep()) continue; collectDots(varp->valuep()); + collectTypedefs(varp->valuep()); enqueueRefs(varp->valuep()); } - if (dotps.empty()) return; + if (dotps.empty() && tdefps.empty()) return; VL_RESTORER(m_modp); m_modp = modp; + // Resolve unresolved class-scoped RefDTypes buried in any typedef reachable from + // the pin/expr tree (e.g. `$bits(wrap_t)` where `wrap_t`'s struct members + // reference `CFG::data_t` from a parameterized-class typedef alias). + for (AstTypedef* const tdefp : tdefps) resolveParamClassRefDType(tdefp->subDTypep()); // Reverse-iterate so inner Dots resolve before outer. for (auto it = dotps.rbegin(); it != dotps.rend(); ++it) resolveDotToTypedef(*it); } diff --git a/test_regress/t/t_class_lparam_nested_chain.py b/test_regress/t/t_class_lparam_nested_chain.py new file mode 100755 index 000000000..46d1fe4c0 --- /dev/null +++ b/test_regress/t/t_class_lparam_nested_chain.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--binary']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_class_lparam_nested_chain.v b/test_regress/t/t_class_lparam_nested_chain.v new file mode 100644 index 000000000..076f9abda --- /dev/null +++ b/test_regress/t/t_class_lparam_nested_chain.v @@ -0,0 +1,72 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Multi-level class hierarchies where one parameterized class's +// localparam value references another parameterized class via its +// own class-scope Dot (e.g. B::width = inner_a::v where inner_a is +// a typedef of A#(...)). resolveDotToTypedef must recursively resolve +// nested class::member Dots in a member's value before constifying, +// otherwise cell-pin deparam fails for the outer Dot. +// +// 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 + +module Sub #( + parameter int WIDTH +) (); +endmodule + +module t; + // Two-level: B's lparam value contains a class::member Dot of its own + virtual class A #( + parameter int x + ); + localparam int v = x * 2; + endclass + + virtual class B #( + parameter int y + ); + typedef A#(y + 1) inner_a; + localparam int width = inner_a::v; // nested Dot inside B's lparam + endclass + + // Three-level: C wraps B wraps A + virtual class C #( + parameter int z + ); + typedef B#(z * 3) inner_b; + localparam int total = inner_b::width; // double-nested Dot + endclass + + typedef B#(5) BInst; + typedef C#(2) CInst; + + // (1) Standalone localparam — resolves via deferred-lparam path + localparam int two_level = BInst::width; // = A#(6)::v = 12 + localparam int three_level = CInst::total; // = B#(6)::width = A#(7)::v = 14 + + // (2) Cell pin uses the nested-class value directly — exercises the + // recursive Dot resolution inside resolveDotToTypedef + Sub #(BInst::width) m_pin_direct (); + Sub #(CInst::total) m_pin_deep (); + Sub #(BInst::width + 1) m_pin_expr (); + + // (3) Cell pin chains through a deferred lparam whose value is + // itself a nested-class Dot + localparam int chain = BInst::width; + Sub #(chain) m_pin_chain (); + + initial begin + `checkh(two_level, 32'd12); + `checkh(three_level, 32'd14); + `checkh(chain, 32'd12); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule diff --git a/test_regress/t/t_class_lparam_struct_member_chain.py b/test_regress/t/t_class_lparam_struct_member_chain.py new file mode 100644 index 000000000..46d1fe4c0 --- /dev/null +++ b/test_regress/t/t_class_lparam_struct_member_chain.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--binary']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_class_lparam_struct_member_chain.v b/test_regress/t/t_class_lparam_struct_member_chain.v new file mode 100644 index 000000000..7affc9a07 --- /dev/null +++ b/test_regress/t/t_class_lparam_struct_member_chain.v @@ -0,0 +1,54 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// A struct typedef whose member references a class-scope-resolved typedef +// from a parameterized-class typedef alias (e.g. CFG::data_t). The struct +// is consumed by `$bits(...)` on a cell parameter pin, which forces V3Param +// to constify the pin during cell deparameterization. V3Param must follow +// typedef references in the pin tree and resolve the unresolved struct-member +// RefDTypes that target the parameterized class, otherwise V3Width hits +// "REFDTYPE 'data_t' not linked to type" (V3AstNodes.cpp). +// +// 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; + virtual class C #(parameter int W = 1); + typedef logic [W-1:0] data_t; + endclass +endpackage + +module Sink #(parameter int BITS = 1) (); + logic [BITS-1:0] data; +endmodule + +module Sub #(parameter int W = 8) (); + typedef pkg::C#(W) CFG; + + // Struct typedef whose member is a class-scope-resolved typedef from + // a parameterized-class typedef alias. Without the V3Param fix, the + // member's RefDType stays UNLINKED and the cell pin's $bits trips an + // internal error. + typedef struct packed { + CFG::data_t payload; + logic v; + } wrap_t; + + Sink #(.BITS($bits(wrap_t))) u_sink (); +endmodule + +module t; + Sub #(.W(8)) u (); + + initial begin + // $bits(wrap_t) = 8 (payload) + 1 (v) = 9 + `checkh($bits(u.u_sink.data), 32'd9); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule From 04eabc62feaf7fd502cb09e31de819cf877547e7 Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Tue, 9 Jun 2026 13:07:17 -0400 Subject: [PATCH 06/17] descend into dots in typedefs --- test_regress/t/t_class_lparam_iface_chain.v | 4 +- test_regress/t/t_class_lparam_typedef_chain.v | 43 ++++++++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/test_regress/t/t_class_lparam_iface_chain.v b/test_regress/t/t_class_lparam_iface_chain.v index 39444fbf5..5395f3bf9 100644 --- a/test_regress/t/t_class_lparam_iface_chain.v +++ b/test_regress/t/t_class_lparam_iface_chain.v @@ -21,7 +21,7 @@ interface SubIface #( logic [IW-1:0] data; endinterface -// Module that takes an interface port — must elaborate after the iface +// Module that takes an interface port - must elaborate after the iface // cell pin is fully constified. module Consumer ( SubIface si @@ -50,7 +50,7 @@ module t; SubIface #(b8_alias) i_chain (); // Iface pin = expression mixing deferred lparams SubIface #(b8 + b13) i_expr (); - // Iface used as a module port — exercises the iface-cell-first path + // Iface used as a module port - exercises the iface-cell-first path Consumer cons (.si(i_bare)); initial begin diff --git a/test_regress/t/t_class_lparam_typedef_chain.v b/test_regress/t/t_class_lparam_typedef_chain.v index c8ec879f8..aa4039514 100644 --- a/test_regress/t/t_class_lparam_typedef_chain.v +++ b/test_regress/t/t_class_lparam_typedef_chain.v @@ -2,10 +2,13 @@ // // Typedefs and class type parameters that resolve through a // class-scope-resolved localparam (typedef alias of a parameterized -// class, e.g. inst::b). Exercises three sub-cases: +// class, e.g. inst::b). Exercises four sub-cases: // - Deferred lparam used as a packed range bound in a typedef // - Deferred lparam used as a value argument to a parameterized class // - Class-scope-resolved typedef that itself depends on the param +// - Class-scope Dot used DIRECTLY (no intermediate lparam) in a typedef +// range inside a parameterized module, consumed by `$bits` on a child +// cell pin // // This file ONLY is placed under the Creative Commons Public Domain. // SPDX-FileCopyrightText: 2026 Wilson Snyder @@ -16,13 +19,44 @@ `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 +// Sink for sub-case (4): the BITS pin consumes $bits of a typedef whose +// packed range uses class-scope Dots. V3Param must descend into the typedef's +// subDType and resolve those Dots, otherwise V3Width emits "dotted expressions +// in parameters". +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 Sub #( + parameter int W = 8 +) (); + // Inner class — same shape as `t`'s C, repeated here to keep sub-case (4) + // self-contained inside Sub. + virtual class CInner #( + parameter int a + ); + localparam int b = a; + endclass + + typedef CInner#(W) CFG; + + // (4) typedef packed range uses class-scope Dots directly + typedef logic [(CFG::b + CFG::b - 1):0] dot_range_t; + + Sink #(.BITS($bits(dot_range_t))) u_sink (); +endmodule + module t; virtual class C #( parameter int a ); localparam int b = a; typedef logic [a-1:0] inner_t; - // localparam derived from a typedef inside the same class — + // localparam derived from a typedef inside the same class - // resolveDotToTypedef must constify this when reached via CFG::width. localparam int width = $bits(inner_t); endclass @@ -46,6 +80,9 @@ module t; // Wire whose range comes from a deferred lparam logic [b8-1:0] wide_bus; + // (4) parameterized wrapper with a typedef range using class Dots directly + Sub #(.W(8)) u_sub (); + initial begin `checkh(b8, 32'd8); `checkh(from_def_b, 32'd8); @@ -54,6 +91,8 @@ module t; `checkh(data_value, 8'hff); wide_bus = '1; `checkh(wide_bus, 8'hff); + // sub-case (4): $bits(dot_range_t) = CFG::b + CFG::b = 8 + 8 = 16 + `checkh($bits(u_sub.u_sink.data), 32'd16); $write("*-* All Finished *-*\n"); $finish; end From 8f935ec3aaa58eb3b61d4f0d7073e3233ab77c73 Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Tue, 9 Jun 2026 13:08:34 -0400 Subject: [PATCH 07/17] descend into dots used by subDTypes --- src/V3Param.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/V3Param.cpp b/src/V3Param.cpp index ea60bd8f1..0317234b0 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -2232,6 +2232,16 @@ public: collectTypedefs(varp->valuep()); enqueueRefs(varp->valuep()); } + // Descend into each collected typedef's subDType for Dots (e.g. class-scoped + // lparams used in a packed range like `logic [(CFG::pc_width - 1):0]`) and + // nested typedef references. The loop grows tdefps as it goes; the + // `i < tdefps.size()` re-check carries us to fixpoint. + for (size_t i = 0; i < tdefps.size(); ++i) { + AstNodeDType* const subp = tdefps[i]->subDTypep(); + if (!subp) continue; + collectDots(subp); + collectTypedefs(subp); + } if (dotps.empty() && tdefps.empty()) return; VL_RESTORER(m_modp); m_modp = modp; From dce1c8d165af5aeedb270c0ea9ec6839a0f20476 Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Tue, 9 Jun 2026 13:11:14 -0400 Subject: [PATCH 08/17] unify walk into single pass --- src/V3Param.cpp | 66 ++++++++++++++++++++----------------------------- 1 file changed, 27 insertions(+), 39 deletions(-) diff --git a/src/V3Param.cpp b/src/V3Param.cpp index 0317234b0..f91dacfca 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -2190,62 +2190,50 @@ public: }); } - // Resolve `class::member` Dots depended on by `rootp`'s tree, and also recursively through - // VarRefs to all leaf dependencies. Then resolve these to typedefs. + // Walk every subtree reachable from `rootp` for three kinds of deferred work + // V3LinkDot left behind when `class#(...)::member` couldn't yet be resolved: + // (1) `AstDot` with a ClassOrPackageRef lhs — a `class::lparam` value Dot to + // constify (e.g. `c8::b` in a pin expression). + // (2) `AstRefDType` with a typedefp — descend into the typedef's subDType + // to find buried Dots (typedef range like `logic [(CFG::w - 1):0]`) and + // unresolved class-scoped struct-member RefDTypes (`CFG::input_meta_t` + // as a struct member). + // (3) `AstVarRef` to a deferred lparam — descend into its valuep so the + // chain `pin -> lparam -> ... -> class::lparam Dot` reaches the Dot. + // Discoveries of (2) and (3) feed back into the walk worklist; (1) and (2) + // are also kept in lists for the resolution passes at the end. void resolveDeferredDotsReachableFrom(AstNode* rootp, AstNodeModule* modp) { std::vector dotps; std::vector tdefps; const auto& deferredVarps = v3Global.rootp()->deferredParamVarps(); std::set reachedDeferred; std::set reachedTypedefs; - auto collectDots = [&](AstNode* p) { + std::vector worklist{rootp}; + while (!worklist.empty()) { + AstNode* const p = worklist.back(); + worklist.pop_back(); p->foreach([&](AstDot* dotp) { if (VN_IS(dotp->lhsp(), ClassOrPackageRef)) dotps.push_back(dotp); }); - }; - auto collectTypedefs = [&](AstNode* p) { p->foreach([&](const AstRefDType* refp) { AstTypedef* const tdefp = refp->typedefp(); - if (tdefp && reachedTypedefs.insert(tdefp).second) tdefps.push_back(tdefp); - }); - }; - std::vector workVarps; - auto enqueueRefs = [&](AstNode* p) { - p->foreach([&](const AstVarRef* refp) { - AstVar* const refVarp = refp->varp(); - if (refVarp && refVarp->varType() == VVarType::LPARAM - && deferredVarps.count(refVarp) && reachedDeferred.insert(refVarp).second) { - workVarps.push_back(refVarp); + if (tdefp && reachedTypedefs.insert(tdefp).second) { + tdefps.push_back(tdefp); + if (tdefp->subDTypep()) worklist.push_back(tdefp->subDTypep()); + } + }); + p->foreach([&](const AstVarRef* refp) { + AstVar* const varp = refp->varp(); + if (varp && varp->varType() == VVarType::LPARAM && deferredVarps.count(varp) + && reachedDeferred.insert(varp).second && varp->valuep()) { + worklist.push_back(varp->valuep()); } }); - }; - collectDots(rootp); - collectTypedefs(rootp); - enqueueRefs(rootp); - // Handle deferred VarRefs recursively until no more, accumulating Dots and - // typedef references along the way. - while (!workVarps.empty()) { - AstVar* const varp = workVarps.back(); - workVarps.pop_back(); - if (!varp->valuep()) continue; - collectDots(varp->valuep()); - collectTypedefs(varp->valuep()); - enqueueRefs(varp->valuep()); - } - // Descend into each collected typedef's subDType for Dots (e.g. class-scoped - // lparams used in a packed range like `logic [(CFG::pc_width - 1):0]`) and - // nested typedef references. The loop grows tdefps as it goes; the - // `i < tdefps.size()` re-check carries us to fixpoint. - for (size_t i = 0; i < tdefps.size(); ++i) { - AstNodeDType* const subp = tdefps[i]->subDTypep(); - if (!subp) continue; - collectDots(subp); - collectTypedefs(subp); } if (dotps.empty() && tdefps.empty()) return; VL_RESTORER(m_modp); m_modp = modp; - // Resolve unresolved class-scoped RefDTypes buried in any typedef reachable from + // Link unresolved class-scoped RefDTypes buried in any typedef reachable from // the pin/expr tree (e.g. `$bits(wrap_t)` where `wrap_t`'s struct members // reference `CFG::data_t` from a parameterized-class typedef alias). for (AstTypedef* const tdefp : tdefps) resolveParamClassRefDType(tdefp->subDTypep()); From af121fcb3e000fe743f930956675ed78db011236 Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Tue, 9 Jun 2026 13:24:39 -0400 Subject: [PATCH 09/17] struct access --- src/V3Param.cpp | 50 +++++++++++++-- test_regress/t/t_class_lparam_typedef_chain.v | 61 +++++++++---------- 2 files changed, 74 insertions(+), 37 deletions(-) diff --git a/src/V3Param.cpp b/src/V3Param.cpp index f91dacfca..e536de15e 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -1302,8 +1302,50 @@ class ParamProcessor final { V3Const::constifyParamsEdit(varp); } if (AstConst* const constp = VN_CAST(varp->valuep(), Const)) { - dotp->replaceWith(constp->cloneTree(false)); - VL_DO_DANGLING(dotp->deleteTree(), dotp); + // Walk up any outer `.fieldN[.fieldM...]` chain (struct lparam + // member access like `CFG::cfg.jt.cam_type`) and accumulate the + // packed-struct bit offset. Replace the topmost Dot with a + // Sel(constp, lsb, width) that constify can fold. + int totalLsb = 0; + int sliceWidth = varp->width(); + AstNodeDType* curDTypep = varp->dtypep(); + AstNode* topp = dotp; + AstDot* outerDot = VN_CAST(dotp->backp(), Dot); + while (outerDot && outerDot->lhsp() == topp) { + const AstParseRef* const fieldRef = VN_CAST(outerDot->rhsp(), ParseRef); + if (!fieldRef) break; + AstNodeDType* const skippedp = curDTypep ? curDTypep->skipRefp() : nullptr; + AstNodeUOrStructDType* const structp + = VN_CAST(skippedp, NodeUOrStructDType); + if (!structp) break; + AstMemberDType* foundMemp = nullptr; + for (AstMemberDType* memp = structp->membersp(); memp; + memp = VN_AS(memp->nextp(), MemberDType)) { + if (memp->name() == fieldRef->name()) { + foundMemp = memp; + break; + } + } + if (!foundMemp) break; + totalLsb += foundMemp->lsb(); + sliceWidth = foundMemp->width(); + curDTypep = foundMemp->subDTypep(); + topp = outerDot; + outerDot = VN_CAST(outerDot->backp(), Dot); + } + if (topp == dotp) { + dotp->replaceWith(constp->cloneTree(false)); + VL_DO_DANGLING(dotp->deleteTree(), dotp); + } else { + AstConst* const clonep = static_cast(constp->cloneTree(false)); + AstSel* const selp + = new AstSel{topp->fileline(), clonep, totalLsb, sliceWidth}; + // Preserve the field's actual dtype (e.g. enum) on the Sel so + // assignments to enum-typed pins don't trip ENUMVALUE. + if (curDTypep) selp->dtypep(curDTypep); + topp->replaceWith(selp); + VL_DO_DANGLING(topp->deleteTree(), topp); + } } } } @@ -2190,8 +2232,8 @@ public: }); } - // Walk every subtree reachable from `rootp` for three kinds of deferred work - // V3LinkDot left behind when `class#(...)::member` couldn't yet be resolved: + // Walk every subtree reachable from `rootp` for deferred work V3LinkDot left behind + // when `class#(...)::member` couldn't yet be resolved: // (1) `AstDot` with a ClassOrPackageRef lhs — a `class::lparam` value Dot to // constify (e.g. `c8::b` in a pin expression). // (2) `AstRefDType` with a typedefp — descend into the typedef's subDType diff --git a/test_regress/t/t_class_lparam_typedef_chain.v b/test_regress/t/t_class_lparam_typedef_chain.v index aa4039514..090a33eb2 100644 --- a/test_regress/t/t_class_lparam_typedef_chain.v +++ b/test_regress/t/t_class_lparam_typedef_chain.v @@ -2,13 +2,14 @@ // // Typedefs and class type parameters that resolve through a // class-scope-resolved localparam (typedef alias of a parameterized -// class, e.g. inst::b). Exercises four sub-cases: +// class, e.g. inst::b). Exercises five sub-cases: // - Deferred lparam used as a packed range bound in a typedef // - Deferred lparam used as a value argument to a parameterized class // - Class-scope-resolved typedef that itself depends on the param // - Class-scope Dot used DIRECTLY (no intermediate lparam) in a typedef -// range inside a parameterized module, consumed by `$bits` on a child -// cell pin +// range, consumed by `$bits` on a child cell pin +// - Struct lparam field access via class-scope Dot +// (`CFG::struct_lparam.field`) inside a parameterized module // // This file ONLY is placed under the Creative Commons Public Domain. // SPDX-FileCopyrightText: 2026 Wilson Snyder @@ -19,35 +20,21 @@ `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 -// Sink for sub-case (4): the BITS pin consumes $bits of a typedef whose -// packed range uses class-scope Dots. V3Param must descend into the typedef's -// subDType and resolve those Dots, otherwise V3Width emits "dotted expressions -// in parameters". -module Sink #( - parameter int BITS = 1 -) (); - logic [BITS-1:0] data; -endmodule +module Sub #(parameter int W = 8) (); endmodule -// Parameterized wrapper so the class specialization is deferred until V3Param -// processes the cell instance. -module Sub #( - parameter int W = 8 -) (); - // Inner class — same shape as `t`'s C, repeated here to keep sub-case (4) - // self-contained inside Sub. - virtual class CInner #( - parameter int a - ); - localparam int b = a; - endclass +// Sub-case (5): struct lparam field access via class-scope Dot. Needs a +// parameterized-module wrapper so the class specialization is deferred until +// V3Param processes the cell instance. +typedef struct packed { logic [7:0] hi; logic [7:0] lo; } pair_t; - typedef CInner#(W) CFG; +class P #(parameter pair_t p); + localparam pair_t pp = p; +endclass - // (4) typedef packed range uses class-scope Dots directly - typedef logic [(CFG::b + CFG::b - 1):0] dot_range_t; - - Sink #(.BITS($bits(dot_range_t))) u_sink (); +module PairHolder #(parameter pair_t cfg = '{hi:'d7, lo:'d11}) (); + typedef P#(cfg) PALIAS; + localparam logic [7:0] hi_val = PALIAS::pp.hi; + localparam logic [7:0] lo_val = PALIAS::pp.lo; endmodule module t; @@ -80,8 +67,13 @@ module t; // Wire whose range comes from a deferred lparam logic [b8-1:0] wide_bus; - // (4) parameterized wrapper with a typedef range using class Dots directly - Sub #(.W(8)) u_sub (); + // (4) Cell pin $bits consumes a typedef whose packed range is built from + // class-scope Dots directly (no intermediate deferred lparam). + typedef logic [(c8::b + c8::b - 1):0] direct_use_t; + Sub #(.W($bits(direct_use_t))) u_sub (); + + // (5) struct lparam field access via class-scope Dot + PairHolder #(.cfg('{hi:'d7, lo:'d11})) u_ph (); initial begin `checkh(b8, 32'd8); @@ -91,8 +83,11 @@ module t; `checkh(data_value, 8'hff); wide_bus = '1; `checkh(wide_bus, 8'hff); - // sub-case (4): $bits(dot_range_t) = CFG::b + CFG::b = 8 + 8 = 16 - `checkh($bits(u_sub.u_sink.data), 32'd16); + // sub-case (4): $bits(direct_use_t) = c8::b + c8::b = 8 + 8 = 16 + `checkh($bits(direct_use_t), 32'd16); + // sub-case (5): struct lparam field access folds to the right field + `checkh(u_ph.hi_val, 8'd7); + `checkh(u_ph.lo_val, 8'd11); $write("*-* All Finished *-*\n"); $finish; end From 59863f43dc3d58595799fd59b4d2b5a2c534a9a4 Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Tue, 9 Jun 2026 13:46:58 -0400 Subject: [PATCH 10/17] lint --- src/V3Param.cpp | 6 +-- test_regress/t/t_class_lparam_nested_chain.v | 4 +- .../t/t_class_lparam_struct_member_chain.v | 42 +++++++++---------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/V3Param.cpp b/src/V3Param.cpp index e536de15e..fce43f780 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -2234,13 +2234,13 @@ public: // Walk every subtree reachable from `rootp` for deferred work V3LinkDot left behind // when `class#(...)::member` couldn't yet be resolved: - // (1) `AstDot` with a ClassOrPackageRef lhs — a `class::lparam` value Dot to + // (1) `AstDot` with a ClassOrPackageRef lhs - a `class::lparam` value Dot to // constify (e.g. `c8::b` in a pin expression). - // (2) `AstRefDType` with a typedefp — descend into the typedef's subDType + // (2) `AstRefDType` with a typedefp - descend into the typedef's subDType // to find buried Dots (typedef range like `logic [(CFG::w - 1):0]`) and // unresolved class-scoped struct-member RefDTypes (`CFG::input_meta_t` // as a struct member). - // (3) `AstVarRef` to a deferred lparam — descend into its valuep so the + // (3) `AstVarRef` to a deferred lparam - descend into its valuep so the // chain `pin -> lparam -> ... -> class::lparam Dot` reaches the Dot. // Discoveries of (2) and (3) feed back into the walk worklist; (1) and (2) // are also kept in lists for the resolution passes at the end. diff --git a/test_regress/t/t_class_lparam_nested_chain.v b/test_regress/t/t_class_lparam_nested_chain.v index 076f9abda..b9607b634 100644 --- a/test_regress/t/t_class_lparam_nested_chain.v +++ b/test_regress/t/t_class_lparam_nested_chain.v @@ -47,11 +47,11 @@ module t; typedef B#(5) BInst; typedef C#(2) CInst; - // (1) Standalone localparam — resolves via deferred-lparam path + // (1) Standalone localparam - resolves via deferred-lparam path localparam int two_level = BInst::width; // = A#(6)::v = 12 localparam int three_level = CInst::total; // = B#(6)::width = A#(7)::v = 14 - // (2) Cell pin uses the nested-class value directly — exercises the + // (2) Cell pin uses the nested-class value directly - exercises the // recursive Dot resolution inside resolveDotToTypedef Sub #(BInst::width) m_pin_direct (); Sub #(CInst::total) m_pin_deep (); diff --git a/test_regress/t/t_class_lparam_struct_member_chain.v b/test_regress/t/t_class_lparam_struct_member_chain.v index 7affc9a07..9c044821a 100644 --- a/test_regress/t/t_class_lparam_struct_member_chain.v +++ b/test_regress/t/t_class_lparam_struct_member_chain.v @@ -18,37 +18,37 @@ // verilog_format: on package pkg; - virtual class C #(parameter int W = 1); - typedef logic [W-1:0] data_t; - endclass + virtual class C #(parameter int W = 1); + typedef logic [W-1:0] data_t; + endclass endpackage module Sink #(parameter int BITS = 1) (); - logic [BITS-1:0] data; + logic [BITS-1:0] data; endmodule module Sub #(parameter int W = 8) (); - typedef pkg::C#(W) CFG; + typedef pkg::C#(W) CFG; - // Struct typedef whose member is a class-scope-resolved typedef from - // a parameterized-class typedef alias. Without the V3Param fix, the - // member's RefDType stays UNLINKED and the cell pin's $bits trips an - // internal error. - typedef struct packed { - CFG::data_t payload; - logic v; - } wrap_t; + // Struct typedef whose member is a class-scope-resolved typedef from + // a parameterized-class typedef alias. Without the V3Param fix, the + // member's RefDType stays UNLINKED and the cell pin's $bits trips an + // internal error. + typedef struct packed { + CFG::data_t payload; + logic v; + } wrap_t; - Sink #(.BITS($bits(wrap_t))) u_sink (); + Sink #(.BITS($bits(wrap_t))) u_sink (); endmodule module t; - Sub #(.W(8)) u (); + Sub #(.W(8)) u (); - initial begin - // $bits(wrap_t) = 8 (payload) + 1 (v) = 9 - `checkh($bits(u.u_sink.data), 32'd9); - $write("*-* All Finished *-*\n"); - $finish; - end + initial begin + // $bits(wrap_t) = 8 (payload) + 1 (v) = 9 + `checkh($bits(u.u_sink.data), 32'd9); + $write("*-* All Finished *-*\n"); + $finish; + end endmodule From 28dbecbe8bf7285a457b4cb82e3bdc8067a71089 Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Thu, 11 Jun 2026 09:57:10 -0400 Subject: [PATCH 11/17] address comments --- src/V3Param.cpp | 49 ++++++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/V3Param.cpp b/src/V3Param.cpp index fce43f780..ca3b6df2c 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -1305,7 +1305,7 @@ class ParamProcessor final { // Walk up any outer `.fieldN[.fieldM...]` chain (struct lparam // member access like `CFG::cfg.jt.cam_type`) and accumulate the // packed-struct bit offset. Replace the topmost Dot with a - // Sel(constp, lsb, width) that constify can fold. + // Sel(constp, lsb, width) matching V3Width::memberSelStruct. int totalLsb = 0; int sliceWidth = varp->width(); AstNodeDType* curDTypep = varp->dtypep(); @@ -1318,14 +1318,8 @@ class ParamProcessor final { AstNodeUOrStructDType* const structp = VN_CAST(skippedp, NodeUOrStructDType); if (!structp) break; - AstMemberDType* foundMemp = nullptr; - for (AstMemberDType* memp = structp->membersp(); memp; - memp = VN_AS(memp->nextp(), MemberDType)) { - if (memp->name() == fieldRef->name()) { - foundMemp = memp; - break; - } - } + AstMemberDType* const foundMemp = VN_CAST( + m_memberMap.findMember(structp, fieldRef->name()), MemberDType); if (!foundMemp) break; totalLsb += foundMemp->lsb(); sliceWidth = foundMemp->width(); @@ -1340,9 +1334,10 @@ class ParamProcessor final { AstConst* const clonep = static_cast(constp->cloneTree(false)); AstSel* const selp = new AstSel{topp->fileline(), clonep, totalLsb, sliceWidth}; - // Preserve the field's actual dtype (e.g. enum) on the Sel so - // assignments to enum-typed pins don't trip ENUMVALUE. - if (curDTypep) selp->dtypep(curDTypep); + // Match V3Width::memberSelStruct: skip RefDTypes to surface + // enum dtype, and mark didWidth so V3Width doesn't reflatten. + if (curDTypep) selp->dtypep(curDTypep->skipRefToEnump()); + selp->didWidth(true); topp->replaceWith(selp); VL_DO_DANGLING(topp->deleteTree(), topp); } @@ -2254,21 +2249,21 @@ public: while (!worklist.empty()) { AstNode* const p = worklist.back(); worklist.pop_back(); - p->foreach([&](AstDot* dotp) { - if (VN_IS(dotp->lhsp(), ClassOrPackageRef)) dotps.push_back(dotp); - }); - p->foreach([&](const AstRefDType* refp) { - AstTypedef* const tdefp = refp->typedefp(); - if (tdefp && reachedTypedefs.insert(tdefp).second) { - tdefps.push_back(tdefp); - if (tdefp->subDTypep()) worklist.push_back(tdefp->subDTypep()); - } - }); - p->foreach([&](const AstVarRef* refp) { - AstVar* const varp = refp->varp(); - if (varp && varp->varType() == VVarType::LPARAM && deferredVarps.count(varp) - && reachedDeferred.insert(varp).second && varp->valuep()) { - worklist.push_back(varp->valuep()); + p->foreach([&](AstNode* np) { + if (AstDot* const dotp = VN_CAST(np, Dot)) { + if (VN_IS(dotp->lhsp(), ClassOrPackageRef)) dotps.push_back(dotp); + } else if (const AstRefDType* const refp = VN_CAST(np, RefDType)) { + AstTypedef* const tdefp = refp->typedefp(); + if (tdefp && reachedTypedefs.insert(tdefp).second) { + tdefps.push_back(tdefp); + if (tdefp->subDTypep()) worklist.push_back(tdefp->subDTypep()); + } + } else if (const AstVarRef* const refp = VN_CAST(np, VarRef)) { + AstVar* const varp = refp->varp(); + if (varp && varp->varType() == VVarType::LPARAM && deferredVarps.count(varp) + && reachedDeferred.insert(varp).second && varp->valuep()) { + worklist.push_back(varp->valuep()); + } } }); } From 55de902d55f7312606074306bd14671b4b3ab471 Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Thu, 2 Jul 2026 10:55:17 -0400 Subject: [PATCH 12/17] address comments --- src/V3Param.cpp | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/V3Param.cpp b/src/V3Param.cpp index ca3b6df2c..19afee32d 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -1293,7 +1293,7 @@ class ParamProcessor final { // member's expression references another paramed class), resolve // those first so constify can fold the whole chain. std::vector nestedDots; - varp->valuep()->foreach([&](AstDot* dp) { + varp->valuep()->foreach([&](AstDot* const dp) { if (VN_IS(dp->lhsp(), ClassOrPackageRef)) nestedDots.push_back(dp); }); for (auto it = nestedDots.rbegin(); it != nestedDots.rend(); ++it) { @@ -1310,22 +1310,25 @@ class ParamProcessor final { int sliceWidth = varp->width(); AstNodeDType* curDTypep = varp->dtypep(); AstNode* topp = dotp; - AstDot* outerDot = VN_CAST(dotp->backp(), Dot); - while (outerDot && outerDot->lhsp() == topp) { - const AstParseRef* const fieldRef = VN_CAST(outerDot->rhsp(), ParseRef); - if (!fieldRef) break; + AstDot* outerDotp = VN_CAST(dotp->backp(), Dot); + // On any mismatch (non-ParseRef rhs, non-packed-struct dtype, or + // unknown field name) leave the outer Dots intact so V3Width can + // emit the proper user-facing diagnostic. + while (outerDotp && outerDotp->lhsp() == topp) { + const AstParseRef* const fieldRefp = VN_CAST(outerDotp->rhsp(), ParseRef); + if (!fieldRefp) break; AstNodeDType* const skippedp = curDTypep ? curDTypep->skipRefp() : nullptr; AstNodeUOrStructDType* const structp = VN_CAST(skippedp, NodeUOrStructDType); if (!structp) break; AstMemberDType* const foundMemp = VN_CAST( - m_memberMap.findMember(structp, fieldRef->name()), MemberDType); + m_memberMap.findMember(structp, fieldRefp->name()), MemberDType); if (!foundMemp) break; totalLsb += foundMemp->lsb(); sliceWidth = foundMemp->width(); curDTypep = foundMemp->subDTypep(); - topp = outerDot; - outerDot = VN_CAST(outerDot->backp(), Dot); + topp = outerDotp; + outerDotp = VN_CAST(outerDotp->backp(), Dot); } if (topp == dotp) { dotp->replaceWith(constp->cloneTree(false)); @@ -1336,8 +1339,11 @@ class ParamProcessor final { = new AstSel{topp->fileline(), clonep, totalLsb, sliceWidth}; // Match V3Width::memberSelStruct: skip RefDTypes to surface // enum dtype, and mark didWidth so V3Width doesn't reflatten. - if (curDTypep) selp->dtypep(curDTypep->skipRefToEnump()); - selp->didWidth(true); + // Only mark didWidth when we have a resolved dtype to attach. + if (curDTypep) { + selp->dtypep(curDTypep->skipRefToEnump()); + selp->didWidth(true); + } topp->replaceWith(selp); VL_DO_DANGLING(topp->deleteTree(), topp); } @@ -2247,20 +2253,20 @@ public: std::set reachedTypedefs; std::vector worklist{rootp}; while (!worklist.empty()) { - AstNode* const p = worklist.back(); + AstNode* const curDotp = worklist.back(); worklist.pop_back(); - p->foreach([&](AstNode* np) { + curDotp->foreach([&](AstNode* const np) { if (AstDot* const dotp = VN_CAST(np, Dot)) { if (VN_IS(dotp->lhsp(), ClassOrPackageRef)) dotps.push_back(dotp); } else if (const AstRefDType* const refp = VN_CAST(np, RefDType)) { AstTypedef* const tdefp = refp->typedefp(); if (tdefp && reachedTypedefs.insert(tdefp).second) { tdefps.push_back(tdefp); - if (tdefp->subDTypep()) worklist.push_back(tdefp->subDTypep()); + worklist.push_back(tdefp->subDTypep()); } } else if (const AstVarRef* const refp = VN_CAST(np, VarRef)) { AstVar* const varp = refp->varp(); - if (varp && varp->varType() == VVarType::LPARAM && deferredVarps.count(varp) + if (varp->varType() == VVarType::LPARAM && deferredVarps.count(varp) && reachedDeferred.insert(varp).second && varp->valuep()) { worklist.push_back(varp->valuep()); } From f0f240209f745dbe84572aa9bd9aee0b3ab34702 Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Mon, 6 Jul 2026 10:41:35 -0400 Subject: [PATCH 13/17] UASSERT_OBJ --- src/V3Param.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/V3Param.cpp b/src/V3Param.cpp index 19afee32d..1383ef665 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -2266,8 +2266,9 @@ public: } } else if (const AstVarRef* const refp = VN_CAST(np, VarRef)) { AstVar* const varp = refp->varp(); - if (varp->varType() == VVarType::LPARAM && deferredVarps.count(varp) - && reachedDeferred.insert(varp).second && varp->valuep()) { + if (varp && varp->varType() == VVarType::LPARAM && deferredVarps.count(varp) + && reachedDeferred.insert(varp).second) { + UASSERT_OBJ(varp->valuep(), varp, "VarRef should have non-null valuep"); worklist.push_back(varp->valuep()); } } From b6b745ffa3dffeb5ccd7fa7ad7f3d5a23fff049d Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Mon, 6 Jul 2026 11:11:19 -0400 Subject: [PATCH 14/17] Move curDTypeP test to loop --- src/V3Param.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/V3Param.cpp b/src/V3Param.cpp index 1383ef665..68fe481bd 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -1324,6 +1324,7 @@ class ParamProcessor final { AstMemberDType* const foundMemp = VN_CAST( m_memberMap.findMember(structp, fieldRefp->name()), MemberDType); if (!foundMemp) break; + if (!foundMemp->subDTypep()) break; totalLsb += foundMemp->lsb(); sliceWidth = foundMemp->width(); curDTypep = foundMemp->subDTypep(); @@ -1339,11 +1340,8 @@ class ParamProcessor final { = new AstSel{topp->fileline(), clonep, totalLsb, sliceWidth}; // Match V3Width::memberSelStruct: skip RefDTypes to surface // enum dtype, and mark didWidth so V3Width doesn't reflatten. - // Only mark didWidth when we have a resolved dtype to attach. - if (curDTypep) { - selp->dtypep(curDTypep->skipRefToEnump()); - selp->didWidth(true); - } + selp->dtypep(curDTypep->skipRefToEnump()); + selp->didWidth(true); topp->replaceWith(selp); VL_DO_DANGLING(topp->deleteTree(), topp); } From 14be536c83a5d4c4cc796df73f569abf9d6bba5c Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Wed, 8 Jul 2026 10:18:51 -0400 Subject: [PATCH 15/17] UASSERT_OBJ --- src/V3Param.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/V3Param.cpp b/src/V3Param.cpp index 68fe481bd..e7eeef45a 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -1323,7 +1323,7 @@ class ParamProcessor final { if (!structp) break; AstMemberDType* const foundMemp = VN_CAST( m_memberMap.findMember(structp, fieldRefp->name()), MemberDType); - if (!foundMemp) break; + UASSERT_OBJ(foundMemp, outerDotp, "member not found in struct/union"); if (!foundMemp->subDTypep()) break; totalLsb += foundMemp->lsb(); sliceWidth = foundMemp->width(); From bb2ea625cfb00859256612c340d26e5f4ebfd406 Mon Sep 17 00:00:00 2001 From: Edmund Lam Date: Wed, 8 Jul 2026 11:53:36 -0400 Subject: [PATCH 16/17] add struct field test --- .../t/t_class_lparam_struct_field_chain.py | 18 +++++ .../t/t_class_lparam_struct_field_chain.v | 65 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 test_regress/t/t_class_lparam_struct_field_chain.py create mode 100644 test_regress/t/t_class_lparam_struct_field_chain.v diff --git a/test_regress/t/t_class_lparam_struct_field_chain.py b/test_regress/t/t_class_lparam_struct_field_chain.py new file mode 100644 index 000000000..46d1fe4c0 --- /dev/null +++ b/test_regress/t/t_class_lparam_struct_field_chain.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.compile(verilator_flags2=['--binary']) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_class_lparam_struct_field_chain.v b/test_regress/t/t_class_lparam_struct_field_chain.v new file mode 100644 index 000000000..f78cc96f2 --- /dev/null +++ b/test_regress/t/t_class_lparam_struct_field_chain.v @@ -0,0 +1,65 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// Struct localparam field access via a class-scope Dot used in a cell +// parameter pin (e.g. `CFG::cfg.jt.cam_type`), where CFG is a typedef +// alias of a parameterized class. V3LinkDot defers the `CFG::cfg` Dot +// until post-V3Param; when the cell is deparameterized, resolveDotToTypedef +// folds the class localparam to a Const and must then walk up the outer +// `.field[.field...]` Dot chain, accumulating the packed-struct bit offset, +// and replace the topmost Dot with a Sel(const, lsb, width) matching +// V3Width::memberSelStruct. Without this, the struct-field select on the +// deferred class localparam cannot be constified for the cell pin. +// +// 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; + +class C #(parameter int W = 1); + // Struct localparam whose fields derive from the class parameter. + 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 + +// Parameterized wrapper so the class specialization is deferred until +// V3Param processes the cell instance. +module Mid #(parameter int W = 8) (); + typedef C#(W) CFG; + // (1) single-level struct field access -> one loop iteration + Sub #(int'(CFG::cfg.tag)) u_tag (); + // (2) nested struct field access -> two loop iterations, lsb accumulation + Sub #(int'(CFG::cfg.jt.cam_type)) u_cam (); + Sub #(int'(CFG::cfg.jt.depth)) u_depth (); +endmodule + +module t; + Mid #(.W(8)) u (); + + initial begin + // cfg.tag = W + 2 = 10 (offset above the 16-bit jt struct) + `checkh(u.u_tag.WIDTH, 32'd10); + // cfg.jt.cam_type = W = 8 (nested, lsb 0) + `checkh(u.u_cam.WIDTH, 32'd8); + // cfg.jt.depth = W + 1 = 9 (nested, lsb 8) + `checkh(u.u_depth.WIDTH, 32'd9); + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule From dd1eb2dfe7a6442d831f6746254c0023eafac858 Mon Sep 17 00:00:00 2001 From: UnsignedByte Date: Thu, 9 Jul 2026 15:10:45 -0400 Subject: [PATCH 17/17] more testing --- src/V3Param.cpp | 39 ++++++++++++---- .../t/t_class_lparam_struct_field_chain.v | 11 +++++ .../t_class_lparam_struct_field_chain_bad.out | 14 ++++++ .../t_class_lparam_struct_field_chain_bad.py | 16 +++++++ .../t/t_class_lparam_struct_field_chain_bad.v | 46 +++++++++++++++++++ 5 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 test_regress/t/t_class_lparam_struct_field_chain_bad.out create mode 100644 test_regress/t/t_class_lparam_struct_field_chain_bad.py create mode 100644 test_regress/t/t_class_lparam_struct_field_chain_bad.v diff --git a/src/V3Param.cpp b/src/V3Param.cpp index 4819892a2..e3146dbd7 100644 --- a/src/V3Param.cpp +++ b/src/V3Param.cpp @@ -1311,26 +1311,47 @@ class ParamProcessor final { AstNodeDType* curDTypep = varp->dtypep(); AstNode* topp = dotp; AstDot* outerDotp = VN_CAST(dotp->backp(), Dot); - // On any mismatch (non-ParseRef rhs, non-packed-struct dtype, or - // unknown field name) leave the outer Dots intact so V3Width can - // emit the proper user-facing diagnostic. - while (outerDotp && outerDotp->lhsp() == topp) { + const auto errorRecover = [&](AstDot* const badp, const string& msg) { + badp->v3error(msg); + badp->replaceWith(new AstConst{badp->fileline(), AstConst::Signed32{}, 0}); + VL_DO_DANGLING(badp->deleteTree(), badp); + }; + bool errored = false; + while (outerDotp && outerDotp->lhsp() == topp) { // LCOV_EXCL_BR_LINE const AstParseRef* const fieldRefp = VN_CAST(outerDotp->rhsp(), ParseRef); - if (!fieldRefp) break; - AstNodeDType* const skippedp = curDTypep ? curDTypep->skipRefp() : nullptr; + if (!fieldRefp) { + errorRecover(outerDotp, "Malformed dotted select in parameter value"); + errored = true; + break; + } + UASSERT_OBJ(curDTypep, outerDotp, + "curDTypep null in struct field chain walk"); + AstNodeDType* const skippedp = curDTypep->skipRefp(); AstNodeUOrStructDType* const structp = VN_CAST(skippedp, NodeUOrStructDType); - if (!structp) break; + if (!structp) { + errorRecover(outerDotp, + "Dotted member select on non-struct parameter value"); + errored = true; + break; + } AstMemberDType* const foundMemp = VN_CAST( m_memberMap.findMember(structp, fieldRefp->name()), MemberDType); - UASSERT_OBJ(foundMemp, outerDotp, "member not found in struct/union"); - if (!foundMemp->subDTypep()) break; + if (!foundMemp) { + errorRecover(outerDotp, "Member " + fieldRefp->prettyNameQ() + + " not found in structure"); + errored = true; + break; + } + UASSERT_OBJ(foundMemp->subDTypep(), outerDotp, + "member has null subDTypep"); totalLsb += foundMemp->lsb(); sliceWidth = foundMemp->width(); curDTypep = foundMemp->subDTypep(); topp = outerDotp; outerDotp = VN_CAST(outerDotp->backp(), Dot); } + if (errored) return; if (topp == dotp) { dotp->replaceWith(constp->cloneTree(false)); VL_DO_DANGLING(dotp->deleteTree(), dotp); diff --git a/test_regress/t/t_class_lparam_struct_field_chain.v b/test_regress/t/t_class_lparam_struct_field_chain.v index f78cc96f2..d5b924943 100644 --- a/test_regress/t/t_class_lparam_struct_field_chain.v +++ b/test_regress/t/t_class_lparam_struct_field_chain.v @@ -35,6 +35,12 @@ class C #(parameter int W = 1); tag: W[7:0] + 8'd2}; endclass +// Wrapper class containing the use of another class +class D #(parameter int W = 1); + typedef C#(W) CC; + localparam int q = int'(CC::cfg.tag) + 100; +endclass + module Sub #(parameter int WIDTH = 0) (); endmodule @@ -47,6 +53,9 @@ module Mid #(parameter int W = 8) (); // (2) nested struct field access -> two loop iterations, lsb accumulation Sub #(int'(CFG::cfg.jt.cam_type)) u_cam (); Sub #(int'(CFG::cfg.jt.depth)) u_depth (); + // (3) nested struct-field Dot buried in a wrapper class's lparam value + typedef D#(W) DD; + Sub #(int'(DD::q)) u_q (); endmodule module t; @@ -59,6 +68,8 @@ module t; `checkh(u.u_cam.WIDTH, 32'd8); // cfg.jt.depth = W + 1 = 9 (nested, lsb 8) `checkh(u.u_depth.WIDTH, 32'd9); + // q = cfg.tag + 100 = 10 + 100 = 110 + `checkh(u.u_q.WIDTH, 32'd110); $write("*-* All Finished *-*\n"); $finish; end diff --git a/test_regress/t/t_class_lparam_struct_field_chain_bad.out b/test_regress/t/t_class_lparam_struct_field_chain_bad.out new file mode 100644 index 000000000..d84a5df97 --- /dev/null +++ b/test_regress/t/t_class_lparam_struct_field_chain_bad.out @@ -0,0 +1,14 @@ +%Error: t/t_class_lparam_struct_field_chain_bad.v:37:25: Malformed dotted select in parameter value + : ... note: In instance 't.u' + 37 | Sub #(int'(CFG::cfg.jt.cam_type[0])) u_fieldref (); + | ^ + ... 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:39:26: Dotted member select on non-struct parameter value + : ... note: In instance 't.u' + 39 | Sub #(int'(CFG::cfg.tag.bogus)) u_nonstruct (); + | ^ +%Error: t/t_class_lparam_struct_field_chain_bad.v:41:25: Member 'nosuchfield' not found in structure + : ... note: In instance 't.u' + 41 | Sub #(int'(CFG::cfg.jt.nosuchfield)) u_nomember (); + | ^ +%Error: Exiting due to diff --git a/test_regress/t/t_class_lparam_struct_field_chain_bad.py b/test_regress/t/t_class_lparam_struct_field_chain_bad.py new file mode 100644 index 000000000..38cf36b43 --- /dev/null +++ b/test_regress/t/t_class_lparam_struct_field_chain_bad.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('linter') + +test.lint(fails=True, expect_filename=test.golden_filename) + +test.passes() diff --git a/test_regress/t/t_class_lparam_struct_field_chain_bad.v b/test_regress/t/t_class_lparam_struct_field_chain_bad.v new file mode 100644 index 000000000..00cf7abfe --- /dev/null +++ b/test_regress/t/t_class_lparam_struct_field_chain_bad.v @@ -0,0 +1,46 @@ +// 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 three +// mismatch guards in the field-chain walk: +// (1) outer Dot rhs is not a plain field identifier (bit-select) +// (2) `.field` applied to a non-struct value +// (3) 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) bit-select after the field chain: outer Dot rhs is not a ParseRef + Sub #(int'(CFG::cfg.jt.cam_type[0])) u_fieldref (); + // (2) `.bogus` on the scalar field `tag`: dotting into a non-struct + Sub #(int'(CFG::cfg.tag.bogus)) u_nonstruct (); + // (3) `.nosuchfield` not a member of struct `jt` + Sub #(int'(CFG::cfg.jt.nosuchfield)) u_nomember (); +endmodule + +module t; + Mid #(.W(8)) u (); +endmodule