From 0d2fcfd49d6aafef8c622b1f4f0bae9ea2e93b00 Mon Sep 17 00:00:00 2001 From: Wilson Snyder Date: Thu, 19 Feb 2026 20:15:37 -0500 Subject: [PATCH] Fix circular class reference %p-printing causing infinite recursion (#7106). Fixes #7106. --- Changes | 1 + include/verilated_types.h | 27 ++++++++++ src/V3AstNodeExpr.h | 4 +- src/V3AstNodeOther.h | 7 +++ src/V3AstNodes.cpp | 2 + src/V3Common.cpp | 27 ++-------- src/V3EmitCBase.cpp | 3 +- src/V3EmitCHeaders.cpp | 5 ++ test_regress/t/t_class_format.out | 3 ++ test_regress/t/t_class_format.v | 86 +++++++++++++++++++------------ 10 files changed, 106 insertions(+), 59 deletions(-) diff --git a/Changes b/Changes index 79efafd50..f4bbcabd0 100644 --- a/Changes +++ b/Changes @@ -114,6 +114,7 @@ Verilator 5.045 devel * Fix constant propagating DPI-written variables (#7074). [Geza Lore, Testorrent USA, Inc.] * Fix conditional expressions in constraints (#7087). [Ryszard Rozak, Antmicro Ltd.] * Fix time to not advance after `$finish` (#7095). +* Fix circular class reference %p-printing causing infinite recursion (#7106). Verilator 5.044 2026-01-01 diff --git a/include/verilated_types.h b/include/verilated_types.h index 558485e48..09930306f 100644 --- a/include/verilated_types.h +++ b/include/verilated_types.h @@ -1884,6 +1884,9 @@ public: VlClass() {} VlClass(const VlClass& copied) {} ~VlClass() override = default; + // METHODS + virtual const char* typeName() const { return "VlClass"; } + virtual std::string to_string() const { return ""; } }; //=================================================================== @@ -2045,6 +2048,30 @@ static inline bool VL_CAST_DYNAMIC(VlNull in, VlClassRef& outr) { return true; } +// For printing class references under a container, several choices: +// 1. Dump recursively the pointed-to object. Can be huge. Might be circular. +// 2. Print object type and pointer as C pointer. Astable when rerun. +// 3. Print object type and pointer as an incrementing number. Needs num storage. +// 4. Print object type alone. Avoids above issues. +template +inline std::string VL_TO_STRING(const VlClassRef& obj) { + return obj ? obj->typeName() : "null"; +} +// Entry point for string conversion (called from not under a container); +// dereference VlClassRef objects to print members +template // Default if no specialization +inline std::string VL_TO_STRING_DEREF(T_Lhs obj) { + return VL_TO_STRING(obj); +} +template // Specialization +inline std::string VL_TO_STRING_DEREF(const VlClassRef& obj) { + return obj ? obj->to_string() : "null"; +} +template // Specialization +inline std::string VL_TO_STRING_DEREF(VlClassRef& obj) { + return obj ? obj->to_string() : "null"; +} + //============================================================================= // VlSampleQueue stores samples for input clockvars in clocking blocks. At a clocking event, // samples from this queue should be written to the correct input clockvar. diff --git a/src/V3AstNodeExpr.h b/src/V3AstNodeExpr.h index 59675732f..9576c4369 100644 --- a/src/V3AstNodeExpr.h +++ b/src/V3AstNodeExpr.h @@ -5553,7 +5553,9 @@ public: ASTGEN_MEMBERS_AstToStringN; void numberOperate(V3Number& out, const V3Number& lhs) override { V3ERROR_NA; } string emitVerilog() override { return "$sformatf(\"%p\", %l)"; } - string emitC() override { return isWide() ? "VL_TO_STRING_W(%nw, %li)" : "VL_TO_STRING(%li)"; } + string emitC() override { + return isWide() ? "VL_TO_STRING_W(%nw, %li)" : "VL_TO_STRING_DEREF(%li)"; + } bool cleanOut() const override { return true; } bool cleanLhs() const override { return true; } bool sizeMattersLhs() const override { return false; } diff --git a/src/V3AstNodeOther.h b/src/V3AstNodeOther.h index 932aedaa9..c39932f6a 100644 --- a/src/V3AstNodeOther.h +++ b/src/V3AstNodeOther.h @@ -488,6 +488,7 @@ class AstCFunc final : public AstNode { // an explicitly passed 'self' pointer as the first argument. This can // be slightly faster due to __restrict, and we do not declare in header // so adding/removing loose functions doesn't recompile everything. + bool m_isOverride : 1; // Override virtual function bool m_isVirtual : 1; // Virtual function bool m_entryPoint : 1; // User may call into this top level function bool m_dpiPure : 1; // Pure DPI function @@ -519,6 +520,7 @@ public: m_isDestructor = false; m_isMethod = true; m_isLoose = false; + m_isOverride = false; m_isVirtual = false; m_needProcess = false; m_entryPoint = false; @@ -582,6 +584,11 @@ public: bool isLoose() const { return m_isLoose; } void isLoose(bool flag) { m_isLoose = flag; } bool isProperMethod() const { return isMethod() && !isLoose(); } + bool isOverride() const { return m_isOverride; } + void isOverride(bool flag) { + m_isOverride = flag; + if (flag) isVirtual(true); + } bool isVirtual() const { return m_isVirtual; } void isVirtual(bool flag) { m_isVirtual = flag; } bool needProcess() const { return m_needProcess; } diff --git a/src/V3AstNodes.cpp b/src/V3AstNodes.cpp index afd4a978d..02795920d 100644 --- a/src/V3AstNodes.cpp +++ b/src/V3AstNodes.cpp @@ -3233,6 +3233,7 @@ void AstCFunc::dump(std::ostream& str) const { if (isDestructor()) str << " [DTOR]"; if (isMethod()) str << " [METHOD]"; if (isLoose()) str << " [LOOSE]"; + if (isOverride()) str << " [OVERRIDE]"; if (isVirtual()) str << " [VIRT]"; if (isCoroutine()) str << " [CORO]"; if (needProcess()) str << " [NPRC]"; @@ -3249,6 +3250,7 @@ void AstCFunc::dumpJson(std::ostream& str) const { dumpJsonBoolFuncIf(str, dpiContext); dumpJsonBoolFuncIf(str, isConstructor); dumpJsonBoolFuncIf(str, isDestructor); + dumpJsonBoolFuncIf(str, isOverride); dumpJsonBoolFuncIf(str, isVirtual); dumpJsonBoolFuncIf(str, isCoroutine); dumpJsonBoolFuncIf(str, needProcess); diff --git a/src/V3Common.cpp b/src/V3Common.cpp index a9c7d4e90..fdb66d65c 100644 --- a/src/V3Common.cpp +++ b/src/V3Common.cpp @@ -37,10 +37,6 @@ string V3Common::makeToStringCall(AstNodeDType* nodep, const std::string& lhs) { stmt += "VL_TO_STRING_W("; stmt += cvtToStr(nodep->widthWords()); stmt += ", "; - } else if (VN_IS(nodep->skipRefp(), BasicDType) && nodep->isWide()) { - stmt += "VL_TO_STRING_W("; - stmt += cvtToStr(nodep->widthWords()); - stmt += ", "; } else { stmt += "VL_TO_STRING("; } @@ -49,20 +45,6 @@ string V3Common::makeToStringCall(AstNodeDType* nodep, const std::string& lhs) { return stmt; } -static void makeVlToString(AstClass* nodep) { - AstCFunc* const funcp - = new AstCFunc{nodep->fileline(), "VL_TO_STRING", nullptr, "std::string"}; - funcp->argTypes("const VlClassRef<" + EmitCUtil::prefixNameProtect(nodep) + ">& obj"); - funcp->isMethod(false); - funcp->isConst(false); - funcp->isStatic(false); - funcp->protect(false); - AstNodeExpr* const exprp - = new AstCExpr{nodep->fileline(), "obj ? obj->to_string() : \"null\""}; - exprp->dtypeSetString(); - funcp->addStmtsp(new AstCReturn{nodep->fileline(), exprp}); - nodep->addStmtsp(funcp); -} static void makeVlToString(AstIface* nodep) { AstCFunc* const funcp = new AstCFunc{nodep->fileline(), "VL_TO_STRING", nullptr, "std::string"}; @@ -112,6 +94,7 @@ static void makeToString(AstClass* nodep) { AstCFunc* const funcp = new AstCFunc{nodep->fileline(), "to_string", nullptr, "std::string"}; funcp->isConst(true); funcp->isStatic(false); + funcp->isOverride(true); funcp->protect(false); AstCExpr* const exprp = new AstCExpr{nodep->fileline(), R"("'{"s + to_string_middle() + "}")"}; exprp->dtypeSetString(); @@ -128,10 +111,9 @@ static void makeToStringMiddle(AstClass* nodep) { std::string comma; for (AstNode* itemp = nodep->membersp(); itemp; itemp = itemp->nextp()) { if (const auto* const varp = VN_CAST(itemp, Var)) { + const AstBasicDType* const basicp = varp->dtypeSkipRefp()->basicp(); if (!varp->isParam() && !varp->isInternal() - && !(varp->dtypeSkipRefp()->basicp() - && (varp->dtypeSkipRefp()->basicp()->isRandomGenerator() - || varp->dtypeSkipRefp()->basicp()->isStdRandomGenerator()))) { + && !(basicp && (basicp->isRandomGenerator() || basicp->isStdRandomGenerator()))) { string stmt = "out += \""; stmt += comma; comma = ", "; @@ -146,7 +128,7 @@ static void makeToStringMiddle(AstClass* nodep) { } if (nodep->extendsp()) { string stmt = "out += "; - if (!comma.empty()) stmt += "\", \"+ "; + if (!comma.empty()) stmt += "\", \" + "; // comma = ", "; // Nothing further so not needed stmt += EmitCUtil::prefixNameProtect(nodep->extendsp()->dtypep()); stmt += "::to_string_middle();"; @@ -174,7 +156,6 @@ void V3Common::commonAll() { for (AstNode* nodep = v3Global.rootp()->modulesp(); nodep; nodep = nodep->nextp()) { if (AstClass* const classp = VN_CAST(nodep, Class)) { // Create ToString methods - makeVlToString(classp); makeToString(classp); makeToStringMiddle(classp); } else if (AstIface* const ifacep = VN_CAST(nodep, Iface)) { diff --git a/src/V3EmitCBase.cpp b/src/V3EmitCBase.cpp index 616eeebe0..b09447aca 100644 --- a/src/V3EmitCBase.cpp +++ b/src/V3EmitCBase.cpp @@ -153,9 +153,10 @@ void EmitCBaseVisitorConst::emitCFuncDecl(const AstCFunc* funcp, const AstNodeMo if (funcp->isStatic() && funcp->isProperMethod()) putns(funcp, "static "); if (funcp->isVirtual()) { UASSERT_OBJ(funcp->isProperMethod(), funcp, "Virtual function is not a proper method"); - putns(funcp, "virtual "); + if (!funcp->isOverride()) putns(funcp, "virtual "); } emitCFuncHeader(funcp, modp, /* withScope: */ false); + if (funcp->isOverride()) putns(funcp, " override"); if (funcp->emptyBody() && !funcp->isLoose() && !cLinkage) { putns(funcp, " {}\n"); } else { diff --git a/src/V3EmitCHeaders.cpp b/src/V3EmitCHeaders.cpp index 2e7bb3cbb..a7817fd13 100644 --- a/src/V3EmitCHeaders.cpp +++ b/src/V3EmitCHeaders.cpp @@ -183,6 +183,11 @@ class EmitCHeader final : public EmitCConstInit { if (!VN_IS(modp, Class)) { decorateFirst(first, section); puts("void " + protect("__Vconfigure") + "(bool first);\n"); + } else { + decorateFirst(first, section); + const std::string name = V3OutFormatter::quoteNameControls( + VIdProtect::protectWordsIf(modp->prettyName(), v3Global.opt.protectIds())); + puts("const char* typeName() const override { return \""s + name + "\"; }\n"); } if (v3Global.opt.coverage() && !VN_IS(modp, Class)) { diff --git a/test_regress/t/t_class_format.out b/test_regress/t/t_class_format.out index 219ddc86b..8a3a7a480 100644 --- a/test_regress/t/t_class_format.out +++ b/test_regress/t/t_class_format.out @@ -1,4 +1,7 @@ ''{b:'h1, i:'h2a, carray4:'{'h11, 'h22, 'h33, 'h44}, cwide:'{'h0, 'h0}, name:"object_name", r:2.2}' ''{b:'h1, i:'h2a, carray4:'{'h911, 'h922, 'h933, 'h944}, cwide:'{'h0, 'h0}, name:"object_name", r:2.2}' DEBUG: object_name (@0) message +''{m_in_a:'h0, m_b:null, m_in_base:'h0}' +''{m_in_a:'h0, m_b:$unit::ClsB, m_in_base:'h0}' +''{m_in_a:'h0, m_b:$unit::ClsB, m_in_base:'h0}' *-* All Finished *-* diff --git a/test_regress/t/t_class_format.v b/test_regress/t/t_class_format.v index e5339d2ad..a67e334fe 100644 --- a/test_regress/t/t_class_format.v +++ b/test_regress/t/t_class_format.v @@ -4,46 +4,64 @@ // SPDX-FileCopyrightText: 2021 Wilson Snyder // SPDX-License-Identifier: CC0-1.0 -`ifdef verilator - `define stop $stop -`else - `define stop -`endif -`define checks(gotv,expv) do if ((gotv) != (expv)) begin $write("%%Error: %s:%0d: got='%s' exp='%s'\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); - class Cls; - bit b; - int i; - bit [15:0] carray4 [4]; - bit [64:0] cwide[2]; - string name; - real r; - task debug(); - $display("DEBUG: %s (@%0t) %s", this.name, $realtime, "message"); - endtask + bit b; + int i; + bit [15:0] carray4[4]; + bit [64:0] cwide[2]; + string name; + real r; + task debug(); + $display("DEBUG: %s (@%0t) %s", this.name, $realtime, "message"); + endtask +endclass + +class Base; + int m_in_base; +endclass + +class ClsA extends Base; + int m_in_a; + Base m_b; +endclass + +class ClsB extends Base; + int m_in_b; + Base m_a; endclass module t; - initial begin - Cls c; - c = new; - c.b = '1; - c.i = 42; - c.r = 2.2; - c.name = "object_name"; + initial begin + Cls c; + ClsA ca; + ClsB cb; - c.carray4[0] = 16'h11; - c.carray4[1] = 16'h22; - c.carray4[2] = 16'h33; - c.carray4[3] = 16'h44; - $display("'%p'", c); + c = new; + c.b = '1; + c.i = 42; + c.r = 2.2; + c.name = "object_name"; - c.carray4 = '{16'h911, 16'h922, 16'h933, 16'h944}; - $display("'%p'", c); + c.carray4[0] = 16'h11; + c.carray4[1] = 16'h22; + c.carray4[2] = 16'h33; + c.carray4[3] = 16'h44; + $display("'%p'", c); - c.debug(); + c.carray4 = '{16'h911, 16'h922, 16'h933, 16'h944}; + $display("'%p'", c); - $write("*-* All Finished *-*\n"); - $finish; - end + c.debug(); + + ca = new; + $display("'%p'", ca); + cb = new; + ca.m_b = cb; + $display("'%p'", ca); + cb.m_a = ca; // Circular + $display("'%p'", ca); + + $write("*-* All Finished *-*\n"); + $finish; + end endmodule