diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index 36699526d..95a4d35aa 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -1171,16 +1171,18 @@ class ConstraintExprVisitor final : public VNVisitor { // else: Global constraints keep nodep alive for write_var processing relinker.relink(exprp); - // For global constraints: check shared path-level set - // For inline constraints: check per-instance set (each __Vrandwith has own randomizer) - // For class-level constraints: check varp->user3() + // Global / inline / class-level member-select refs key on the full path + // (so same-type sub-objects c1.x, c2.x stay distinct); a plain class-level + // variable keys on user3(). const bool alreadyWritten = isGlobalConstrained ? m_writtenVars.count(smtName) > 0 : m_inlineInitTaskp ? m_inlineWrittenVars.count(smtName) > 0 + : membersel ? m_writtenVars.count(smtName) > 0 : varp->user3(); const bool shouldWriteVar = !alreadyWritten; if (shouldWriteVar) { // Track this variable path as written - if (isGlobalConstrained) m_writtenVars.insert(smtName); + if (isGlobalConstrained || (membersel && !m_inlineInitTaskp)) + m_writtenVars.insert(smtName); if (m_inlineInitTaskp) m_inlineWrittenVars.insert(smtName); // For global constraints, delete nodep after processing if (isGlobalConstrained && !nodep->backp()) VL_DO_DANGLING(pushDeletep(nodep), nodep); @@ -3882,6 +3884,22 @@ class RandomizeVisitor final : public VNVisitor { return VN_IS(dtypep, ClassRefDType); }); } + // True if this class owns a global constraint: a member-select chain rooted + // at a rand class-typed handle reaching into a sub-object. + bool classOwnsGlobalConstraint(const AstClass* classp) const { + return classp->existsMember([](const AstClass*, const AstConstraint* constrp) { + bool owns = false; + constrp->foreach([&](const AstMemberSel* memberSelp) { + const AstNode* rootp = memberSelp->fromp(); + while (const AstMemberSel* const sp = VN_CAST(rootp, MemberSel)) + rootp = sp->fromp(); + if (const AstVarRef* const refp = VN_CAST(rootp, VarRef)) { + if (VN_IS(refp->varp()->dtypep()->skipRefp(), ClassRefDType)) owns = true; + } + }); + return owns; + }); + } // Get or create __VrandCb_pre/__VrandCb_post task for nested callbacks AstTask* getCreateNestedCallbackTask(AstClass* classp, const string& suffix) { const string name = "__VrandCb_" + suffix; @@ -4773,6 +4791,10 @@ class RandomizeVisitor final : public VNVisitor { UINFO(9, "Define randomize() for " << nodep); nodep->baseMostClassp()->needRNG(true); + // Detect global-constraint ownership BEFORE the constraint items are + // unlinked into setup tasks below (after that the member-selects are gone). + const bool basicFirst = classOwnsGlobalConstraint(nodep); + FileLine* fl = nodep->fileline(); AstFunc* const randomizep = V3Randomize::newRandomizeFunc(m_memberMap, nodep); AstVar* const fvarp = VN_AS(randomizep->fvarp(), Var); @@ -5081,29 +5103,41 @@ class RandomizeVisitor final : public VNVisitor { beginValp = new AstConst{fl, AstConst::WidthedValue{}, 32, 1}; } + AstFunc* const basicRandomizep + = V3Randomize::newRandomizeFunc(m_memberMap, nodep, BASIC_RANDOMIZE_FUNC_NAME); + addBasicRandomizeBody(basicRandomizep, nodep, randModeVarp); + // A basicFirst owner basic-randomizes first, then the solver overrides the + // constrained leaves, so a globally-constrained leaf (not user3) is still + // basic-randomized when its type is randomized standalone. AstVarRef* const fvarRefp = new AstVarRef{fl, fvarp, VAccess::WRITE}; - randomizep->addStmtsp(new AstAssign{fl, fvarRefp, beginValp}); + randomizep->addStmtsp(new AstAssign{ + fl, fvarRefp, basicFirst ? new AstFuncRef{fl, basicRandomizep} : beginValp}); const auto sizeArraysIt = m_sizeConstrainedArrays.find(nodep); const bool needsSizePhase = sizeArraysIt != m_sizeConstrainedArrays.end() && !sizeArraysIt->second.empty(); - if (!needsSizePhase) { + // Size-only resize fallback (no element constraint, so no two-pass phase). + // Must run after the solver .next() that sets the size variable; emit it + // after whichever assignment below holds the solver call. + const auto emitResizeFallback = [&]() { + if (needsSizePhase) return; if (AstTask* const resizeAllTaskp = VN_AS(m_memberMap.findMember(nodep, "__Vresize_constrained_arrays"), Task)) { AstTaskRef* const resizeTaskRefp = new AstTaskRef{fl, resizeAllTaskp}; randomizep->addStmtsp(resizeTaskRefp->makeStmt()); } - } + }; + if (!basicFirst) emitResizeFallback(); AstVarRef* const fvarRefReadp = fvarRefp->cloneTree(false); fvarRefReadp->access(VAccess::READ); - AstFunc* const basicRandomizep - = V3Randomize::newRandomizeFunc(m_memberMap, nodep, BASIC_RANDOMIZE_FUNC_NAME); - addBasicRandomizeBody(basicRandomizep, nodep, randModeVarp); - AstFuncRef* const basicRandomizeCallp = new AstFuncRef{fl, basicRandomizep}; + AstNodeExpr* const secondHalfp + = basicFirst ? beginValp : new AstFuncRef{fl, basicRandomizep}; randomizep->addStmtsp(new AstAssign{fl, fvarRefp->cloneTree(false), - new AstAnd{fl, fvarRefReadp, basicRandomizeCallp}}); + new AstAnd{fl, fvarRefReadp, secondHalfp}}); + + if (basicFirst) emitResizeFallback(); // Call nested post_randomize on rand class-type members (IEEE 18.4.1) if (classHasRandClassMembers(nodep)) { @@ -5643,15 +5677,21 @@ public: explicit RandomizeVisitor(AstNetlist* nodep) : m_inlineUniqueNames{"__Vrandwith"} { createRandomizeClassVars(nodep); - // Mark variables in global constraints - // These should not be randomized in nested class's __VBasicRand - nodep->foreach([&](AstConstraint* constrp) { - constrp->foreach([&](AstMemberSel* memberSelp) { - // Only mark if this MemberSel was created during constraint cloning - if (memberSelp->user2p()) { + // Flag local constraint leaves as solver-owned so __VBasicRand skips them. + // Runs before any class is lowered. Only a randomized class counts, and + // only a leaf owned by the constraint's own class: a leaf reached through + // a global constraint stays basic-randomized so a standalone randomize() + // of its type still randomizes it (issue #7833); a class-typed sub-object + // is skipped so its own members are still basic-randomized. + nodep->foreach([&](AstClass* const classp) { + if (!classp->user1()) return; + classp->foreachMember([&](AstClass* const ownerClassp, AstConstraint* const constrp) { + constrp->foreach([&](AstMemberSel* const memberSelp) { AstVar* const varp = memberSelp->varp(); + if (VN_IS(varp->dtypep()->skipRefp(), ClassRefDType)) return; + if (VN_AS(varp->user2p(), NodeModule) != ownerClassp) return; if (!varp->user3()) varp->user3(true); - } + }); }); }); diff --git a/test_regress/t/t_constraint_global_subobj.py b/test_regress/t/t_constraint_global_subobj.py new file mode 100755 index 000000000..db1adb3f9 --- /dev/null +++ b/test_regress/t/t_constraint_global_subobj.py @@ -0,0 +1,21 @@ +#!/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') + +if not test.have_solver: + test.skip("No constraint solver installed") + +test.compile() + +test.execute() + +test.passes() diff --git a/test_regress/t/t_constraint_global_subobj.v b/test_regress/t/t_constraint_global_subobj.v new file mode 100644 index 000000000..333695f2d --- /dev/null +++ b/test_regress/t/t_constraint_global_subobj.v @@ -0,0 +1,178 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +// Scenario 1 (issue #7833 literal): owner SA1 declares a global constraint on a +// sub-object's type but is NEVER randomized; a standalone randomize() of the +// holder must still randomize the nested fields. +class C1; + rand int x; + rand int y; +endclass + +class B1; + rand C1 c; + function new(); + c = new(); + endfunction +endclass + +class SA1; + rand B1 b; + constraint c_foo {b.c.x == 123;} +endclass + +// Scenario 2 (combined): the SAME design randomizes both the constraint owner +// and a standalone holder of the constrained type. +class C2; + rand int x; + rand int y; +endclass + +class B2; + rand C2 c; + function new(); + c = new(); + endfunction +endclass + +class SA2; + rand B2 b; + constraint c_foo {b.c.x == 123;} + function new(); + b = new(); + endfunction +endclass + +// Scenario 3 (multiple same-type sub-objects): two sub-objects of one type are +// each pinned by a global constraint while that type is also randomized +// standalone; the two share one underlying rand variable. +class C3; + rand int x; +endclass + +class M3; + rand C3 c1; + rand C3 c2; + constraint c { + c1.x == 11; + c2.x == 22; + } + function new(); + c1 = new(); + c2 = new(); + endfunction +endclass + +// Scenario 4 (global constraint owner with its own size-constrained array): the +// owner basic-randomizes first, the solver overrides last, and the size-only +// resize fallback still resizes from the solver-determined size. +class C4; + rand int x; +endclass + +class A4; + rand C4 c; + rand int arr[]; + constraint c_sub {c.x == 5;} + constraint c_size {arr.size() == 4;} + function new(); + c = new(); + endfunction +endclass + +module t_constraint_global_subobj; + B1 b1; + SA2 a2; + B2 b2; + M3 m3; + C3 s3; + A4 a4; + int prevx, prevy, p3; + bit varyx, varyy, vary3; + + initial begin + // Scenario 1: SA1 never randomized; standalone B1 must vary both fields. + b1 = new(); + varyx = 0; + varyy = 0; + if (b1.randomize() != 1) $stop; + prevx = b1.c.x; + prevy = b1.c.y; + for (int i = 0; i < 20; i++) begin + if (b1.randomize() != 1) $stop; + if (b1.c.x != prevx) varyx = 1; + if (b1.c.y != prevy) varyy = 1; + prevx = b1.c.x; + prevy = b1.c.y; + end + if (!varyx || !varyy) begin + $display("ERROR: standalone holder fields stuck (varyx=%0d varyy=%0d)", varyx, varyy); + $stop; + end + + // Scenario 2: randomize the owner (constraint applies) and a standalone + // holder (fields random) in the same design. + a2 = new(); + if (a2.randomize() != 1) $stop; + if (a2.b.c.x != 123) begin + $display("ERROR: owner constraint not applied, a2.b.c.x=%0d", a2.b.c.x); + $stop; + end + b2 = new(); + varyx = 0; + if (b2.randomize() != 1) $stop; + prevx = b2.c.x; + for (int i = 0; i < 20; i++) begin + if (b2.randomize() != 1) $stop; + if (b2.c.x != prevx) varyx = 1; + prevx = b2.c.x; + end + if (!varyx) begin + $display("ERROR: standalone holder x stuck while owner also randomized"); + $stop; + end + + // Scenario 3: two same-type sub-objects each pinned, plus standalone vary. + m3 = new(); + if (m3.randomize() != 1) $stop; + if (m3.c1.x != 11) begin + $display("ERROR: m3.c1.x should be 11, got %0d", m3.c1.x); + $stop; + end + if (m3.c2.x != 22) begin + $display("ERROR: m3.c2.x should be 22, got %0d", m3.c2.x); + $stop; + end + s3 = new(); + vary3 = 0; + if (s3.randomize() != 1) $stop; + p3 = s3.x; + for (int i = 0; i < 20; i++) begin + if (s3.randomize() != 1) $stop; + if (s3.x != p3) vary3 = 1; + p3 = s3.x; + end + if (!vary3) begin + $display("ERROR: standalone same-type x stuck"); + $stop; + end + + // Scenario 4: owner with a global constraint AND its own size constraint. + a4 = new(); + if (a4.randomize() != 1) $stop; + if (a4.c.x != 5) begin + $display("ERROR: a4.c.x should be 5, got %0d", a4.c.x); + $stop; + end + if (a4.arr.size() != 4) begin + $display("ERROR: a4.arr.size() should be 4, got %0d", a4.arr.size()); + $stop; + end + + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule