From 5458bd2e77633740cc8838948968afd83c58c8b3 Mon Sep 17 00:00:00 2001 From: Saqib Khan Date: Wed, 30 Sep 2026 16:42:58 -0600 Subject: [PATCH] Fix circular class inheritance hang (#8324) (#8545) --- src/V3LinkDot.cpp | 23 +++++++++++++++ test_regress/t/t_class_extends_cycle_bad.out | 23 +++++++++++++++ test_regress/t/t_class_extends_cycle_bad.py | 16 +++++++++++ test_regress/t/t_class_extends_cycle_bad.v | 30 ++++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 test_regress/t/t_class_extends_cycle_bad.out create mode 100755 test_regress/t/t_class_extends_cycle_bad.py create mode 100644 test_regress/t/t_class_extends_cycle_bad.v diff --git a/src/V3LinkDot.cpp b/src/V3LinkDot.cpp index 8b7df8b58..d08045646 100644 --- a/src/V3LinkDot.cpp +++ b/src/V3LinkDot.cpp @@ -3463,6 +3463,24 @@ class LinkDotResolveVisitor final : public VNVisitor { } defParamPins.emplace(nodep->paramPath(), nodep); } + static bool extendsClass(const AstClass* classp, const AstClass* targetp) { + // Return true if classp inherits from targetp through already resolved extends + // If performance of this becomes a problem, use just a counter to check + // exceeds some number of iterations then perform the more expensive analysis + std::set visited; + std::vector todo{classp}; + while (!todo.empty()) { + const AstClass* const currp = todo.back(); + todo.pop_back(); + if (currp == targetp) return true; + if (!visited.insert(currp).second) continue; + for (const AstClassExtends* cextp = currp->extendsp(); cextp; + cextp = VN_AS(cextp->nextp(), ClassExtends)) { + if (const AstClass* const basep = cextp->classOrNullp()) todo.push_back(basep); + } + } + return false; + } static AstClocking* sensClockingp(AstNode* nodep) { // Return the clocking block referenced by nodep, either directly or through a modport if (AstClocking* const clockingp = VN_CAST(nodep, Clocking)) return clockingp; @@ -5989,6 +6007,11 @@ class LinkDotResolveVisitor final : public VNVisitor { if (baseClassp == nodep) { cextp->v3error("Attempting to extend class " << nodep->prettyNameQ() << " from itself"); + } else if (extendsClass(baseClassp, nodep)) { + cextp->v3error("Attempting to extend class " + << nodep->prettyNameQ() << " from " + << baseClassp->prettyNameQ() + << ", which circularly inherits from it"); } else if (cextp->isImplements() && !baseClassp->isInterfaceClass()) { cextp->v3error("Attempting to implement from non-interface class " << baseClassp->prettyNameQ() << '\n' diff --git a/test_regress/t/t_class_extends_cycle_bad.out b/test_regress/t/t_class_extends_cycle_bad.out new file mode 100644 index 000000000..757c1e583 --- /dev/null +++ b/test_regress/t/t_class_extends_cycle_bad.out @@ -0,0 +1,23 @@ +%Error: t/t_class_extends_cycle_bad.v:10:21: Attempting to extend class 'Cls2B' from 'Cls2A', which circularly inherits from it + 10 | class Cls2B extends Cls2A; + | ^~~~~ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Error: t/t_class_extends_cycle_bad.v:8:21: Attempting to extend class 'Cls2A' from 'Cls2B', which circularly inherits from it + 8 | class Cls2A extends Cls2B; + | ^~~~~ +%Error: t/t_class_extends_cycle_bad.v:16:21: Attempting to extend class 'Cls3B' from 'Cls3A', which circularly inherits from it + 16 | class Cls3B extends Cls3A; + | ^~~~~ +%Error: t/t_class_extends_cycle_bad.v:18:21: Attempting to extend class 'Cls3C' from 'Cls3B', which circularly inherits from it + 18 | class Cls3C extends Cls3B; + | ^~~~~ +%Error: t/t_class_extends_cycle_bad.v:14:21: Attempting to extend class 'Cls3A' from 'Cls3C', which circularly inherits from it + 14 | class Cls3A extends Cls3C; + | ^~~~~ +%Error: t/t_class_extends_cycle_bad.v:24:30: Attempting to extend class 'IfcB' from 'IfcA', which circularly inherits from it + 24 | interface class IfcB extends IfcA; + | ^~~~ +%Error: t/t_class_extends_cycle_bad.v:22:30: Attempting to extend class 'IfcA' from 'IfcB', which circularly inherits from it + 22 | interface class IfcA extends IfcB; + | ^~~~ +%Error: Exiting due to diff --git a/test_regress/t/t_class_extends_cycle_bad.py b/test_regress/t/t_class_extends_cycle_bad.py new file mode 100755 index 000000000..38cf36b43 --- /dev/null +++ b/test_regress/t/t_class_extends_cycle_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_extends_cycle_bad.v b/test_regress/t/t_class_extends_cycle_bad.v new file mode 100644 index 000000000..e211fbe74 --- /dev/null +++ b/test_regress/t/t_class_extends_cycle_bad.v @@ -0,0 +1,30 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Saqib Khan +// SPDX-License-Identifier: CC0-1.0 + +// Two-class cycle +class Cls2A extends Cls2B; +endclass +class Cls2B extends Cls2A; +endclass + +// Three-class cycle +class Cls3A extends Cls3C; +endclass +class Cls3B extends Cls3A; +endclass +class Cls3C extends Cls3B; +endclass + +// Interface class cycle +interface class IfcA extends IfcB; +endclass +interface class IfcB extends IfcA; +endclass + +module t; + Cls2A c2 = new; + Cls3B c3 = new; +endmodule