diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index 097a97994..719094c63 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -947,76 +947,195 @@ class ConstraintExprVisitor final : public VNVisitor { if (isGlobalConstrained) m_writtenVars.insert(smtName); // For global constraints, delete nodep after processing if (isGlobalConstrained && !nodep->backp()) VL_DO_DANGLING(pushDeletep(nodep), nodep); - AstCMethodHard* const methodp = new AstCMethodHard{ - varp->fileline(), - new AstVarRef{varp->fileline(), VN_AS(m_genp->user2p(), NodeModule), m_genp, - VAccess::READWRITE}, - VCMethod::RANDOMIZER_WRITE_VAR}; - uint32_t dimension = 0; - if (VN_IS(varp->dtypep(), UnpackArrayDType) || VN_IS(varp->dtypep(), DynArrayDType) - || VN_IS(varp->dtypep(), QueueDType) || VN_IS(varp->dtypep(), AssocArrayDType)) { - const std::pair dims - = varp->dtypep()->dimensions(/*includeBasic=*/true); - const uint32_t unpackedDimensions = dims.second; - dimension = unpackedDimensions; - } - if (VN_IS(varp->dtypeSkipRefp(), StructDType) - && !VN_AS(varp->dtypeSkipRefp(), StructDType)->packed()) { - VN_AS(varp->dtypeSkipRefp(), StructDType)->markConstrainedRand(true); - dimension = 1; - } - methodp->dtypeSetVoid(); - AstNodeModule* classp; - if (membersel) { - // For membersel, find the root varref to get the class where randomize() is called - AstNode* rootNode = membersel->fromp(); - while (AstMemberSel* nestedMemberSel = VN_CAST(rootNode, MemberSel)) { - rootNode = nestedMemberSel->fromp(); + + // Detect if variable is an array of class references + bool isClassRefArray = false; + AstClassRefDType* elemClassRefDtp = nullptr; + { + AstNodeDType* varDtp = varp->dtypep()->skipRefp(); + if (VN_IS(varDtp, DynArrayDType) || VN_IS(varDtp, QueueDType) + || VN_IS(varDtp, UnpackArrayDType) || VN_IS(varDtp, AssocArrayDType)) { + AstNodeDType* const elemDtp = varDtp->subDTypep()->skipRefp(); + elemClassRefDtp = VN_CAST(elemDtp, ClassRefDType); + if (elemClassRefDtp) isClassRefArray = true; } - if (AstNodeVarRef* rootVarRef = VN_CAST(rootNode, NodeVarRef)) { - classp = VN_AS(rootVarRef->varp()->user2p(), NodeModule); - } else { - classp = VN_AS(membersel->user2p(), NodeModule); + } + + if (isClassRefArray && !membersel) { + // Per-member registration loop for class ref arrays + FileLine* const fl = varp->fileline(); + AstClass* const elemClassp = elemClassRefDtp->classp(); + AstNodeModule* const varClassp = VN_AS(varp->user2p(), NodeModule); + + AstVar* const iterVarp + = new AstVar{fl, VVarType::BLOCKTEMP, "__Vi", varp->findUInt32DType()}; + iterVarp->funcLocal(true); + iterVarp->lifetime(VLifetime::AUTOMATIC_EXPLICIT); + + AstNode* const stmtsp = iterVarp; + stmtsp->addNext(new AstAssign{fl, new AstVarRef{fl, iterVarp, VAccess::WRITE}, + new AstConst{fl, 0}}); + + AstVarRef* const arraySizeRef = new AstVarRef{fl, varClassp, varp, VAccess::READ}; + arraySizeRef->classOrPackagep(classOrPackagep); + AstCMethodHard* const sizep + = new AstCMethodHard{fl, arraySizeRef, VCMethod::DYN_SIZE, nullptr}; + sizep->dtypeSetUInt32(); + + AstLoop* const loopp = new AstLoop{fl}; + stmtsp->addNext(loopp); + loopp->addStmtsp(new AstLoopTest{ + fl, loopp, new AstLt{fl, new AstVarRef{fl, iterVarp, VAccess::READ}, sizep}}); + + AstVarRef* const arrayAtRef = new AstVarRef{fl, varClassp, varp, VAccess::READ}; + arrayAtRef->classOrPackagep(classOrPackagep); + AstCMethodHard* const atReadp + = new AstCMethodHard{fl, arrayAtRef, VCMethod::ARRAY_AT, + new AstVarRef{fl, iterVarp, VAccess::READ}}; + atReadp->dtypep(elemClassRefDtp); + AstIf* const ifNonNullp = new AstIf{ + fl, new AstNeq{fl, atReadp, new AstConst{fl, AstConst::Null{}}}, nullptr}; + loopp->addStmtsp(ifNonNullp); + + AstCStmt* const bufDeclp = new AstCStmt{fl, "char __Vn[256];\n"}; + ifNonNullp->addThensp(bufDeclp); + + // 32-bit index → 8 hex chars for SMT name formatting + constexpr int idxWidth = 32; + const int fmtWidth = VL_WORDS_I(idxWidth) * 8; + + for (const AstClass* cp = elemClassp; cp; + cp = cp->extendsp() ? cp->extendsp()->classp() : nullptr) { + for (AstNode* mnodep = cp->stmtsp(); mnodep; mnodep = mnodep->nextp()) { + AstVar* const memberVarp = VN_CAST(mnodep, Var); + if (!memberVarp || !memberVarp->rand().isRandomizable()) continue; + AstNodeDType* const memberDtp = memberVarp->dtypep()->skipRefp(); + if (VN_IS(memberDtp, ClassRefDType) || VN_IS(memberDtp, DynArrayDType) + || VN_IS(memberDtp, QueueDType) || VN_IS(memberDtp, UnpackArrayDType) + || VN_IS(memberDtp, AssocArrayDType)) + continue; + const int memberWidth = memberDtp->width(); + + AstCStmt* const fmtp = new AstCStmt{fl}; + fmtp->add("VL_SNPRINTF(__Vn, sizeof(__Vn), \"" + smtName + ".%0" + + std::to_string(fmtWidth) + "x." + memberVarp->name() + + "\", (unsigned)"); + fmtp->add(new AstVarRef{fl, iterVarp, VAccess::READ}); + fmtp->add(");\n"); + ifNonNullp->addThensp(fmtp); + + AstVarRef* const arrayWrRef + = new AstVarRef{fl, varClassp, varp, VAccess::WRITE}; + arrayWrRef->classOrPackagep(classOrPackagep); + AstCMethodHard* const atWritep + = new AstCMethodHard{fl, arrayWrRef, VCMethod::ARRAY_AT_WRITE, + new AstVarRef{fl, iterVarp, VAccess::READ}}; + atWritep->dtypep(elemClassRefDtp); + AstMemberSel* const memberSelp + = new AstMemberSel{fl, atWritep, memberVarp}; + + AstCMethodHard* const writeVarp = new AstCMethodHard{ + fl, + new AstVarRef{fl, VN_AS(m_genp->user2p(), NodeModule), m_genp, + VAccess::READWRITE}, + VCMethod::RANDOMIZER_WRITE_VAR}; + writeVarp->dtypeSetVoid(); + writeVarp->addPinsp(memberSelp); + writeVarp->addPinsp(new AstConst{fl, AstConst::Unsized64{}, + static_cast(memberWidth)}); + AstCExpr* const nameRefp = new AstCExpr{fl, AstCExpr::Pure{}, "__Vn", 0}; + nameRefp->dtypep(varp->dtypep()); + writeVarp->addPinsp(nameRefp); + writeVarp->addPinsp(new AstConst{fl, AstConst::Unsized64{}, 0ULL}); + ifNonNullp->addThensp(writeVarp->makeStmt()); + } } - methodp->addPinsp(membersel); - } else { - classp = VN_AS(varp->user2p(), NodeModule); - AstVarRef* const varRefp - = new AstVarRef{varp->fileline(), classp, varp, VAccess::WRITE}; - varRefp->classOrPackagep(classOrPackagep); - methodp->addPinsp(varRefp); - } - AstNodeDType* tmpDtypep = varp->dtypep(); - while (VN_IS(tmpDtypep, UnpackArrayDType) || VN_IS(tmpDtypep, DynArrayDType) - || VN_IS(tmpDtypep, QueueDType) || VN_IS(tmpDtypep, AssocArrayDType)) - tmpDtypep = tmpDtypep->subDTypep(); - const size_t width = tmpDtypep->width(); - methodp->addPinsp( - new AstConst{varp->dtypep()->fileline(), AstConst::Unsized64{}, width}); - AstNodeExpr* const varnamep = new AstCExpr{varp->fileline(), AstCExpr::Pure{}, - "\"" + smtName + "\"", varp->width()}; - varnamep->dtypep(varp->dtypep()); - methodp->addPinsp(varnamep); - methodp->addPinsp( - new AstConst{varp->dtypep()->fileline(), AstConst::Unsized64{}, dimension}); - // Don't pass randMode.index for global constraints with membersel - // because constraint object can't access nested object's randmode array - if (randMode.usesMode && !(isGlobalConstrained && membersel)) { - methodp->addPinsp( - new AstConst{varp->fileline(), AstConst::Unsized64{}, randMode.index}); - } - AstNodeFTask* initTaskp = m_inlineInitTaskp; - if (!initTaskp) { + + loopp->addStmtsp( + new AstAssign{fl, new AstVarRef{fl, iterVarp, VAccess::WRITE}, + new AstAdd{fl, new AstConst{fl, 1}, + new AstVarRef{fl, iterVarp, VAccess::READ}}}); + + AstBegin* const beginp = new AstBegin{fl, "", stmtsp, true}; varp->user3(true); - if (membersel) { - initTaskp = VN_AS(m_memberMap.findMember(classp, "randomize"), NodeFTask); - UASSERT_OBJ(initTaskp, classp, "No randomize() in class"); - } else { - initTaskp = VN_AS(m_memberMap.findMember(classp, "new"), NodeFTask); - UASSERT_OBJ(initTaskp, classp, "No new() in class"); + AstNodeFTask* initTaskp = m_inlineInitTaskp; + if (!initTaskp) { + initTaskp = VN_AS(m_memberMap.findMember(varClassp, "new"), NodeFTask); + UASSERT_OBJ(initTaskp, varClassp, "No new() in class"); } + initTaskp->addStmtsp(beginp); + } else { + AstCMethodHard* const methodp = new AstCMethodHard{ + varp->fileline(), + new AstVarRef{varp->fileline(), VN_AS(m_genp->user2p(), NodeModule), m_genp, + VAccess::READWRITE}, + VCMethod::RANDOMIZER_WRITE_VAR}; + uint32_t dimension = 0; + if (VN_IS(varp->dtypep(), UnpackArrayDType) || VN_IS(varp->dtypep(), DynArrayDType) + || VN_IS(varp->dtypep(), QueueDType) + || VN_IS(varp->dtypep(), AssocArrayDType)) { + const std::pair dims + = varp->dtypep()->dimensions(/*includeBasic=*/true); + const uint32_t unpackedDimensions = dims.second; + dimension = unpackedDimensions; + } + if (VN_IS(varp->dtypeSkipRefp(), StructDType) + && !VN_AS(varp->dtypeSkipRefp(), StructDType)->packed()) { + VN_AS(varp->dtypeSkipRefp(), StructDType)->markConstrainedRand(true); + dimension = 1; + } + methodp->dtypeSetVoid(); + AstNodeModule* classp; + if (membersel) { + // For membersel, find the root varref to get the class + AstNode* rootNode = membersel->fromp(); + while (AstMemberSel* nestedMemberSel = VN_CAST(rootNode, MemberSel)) { + rootNode = nestedMemberSel->fromp(); + } + if (AstNodeVarRef* rootVarRef = VN_CAST(rootNode, NodeVarRef)) { + classp = VN_AS(rootVarRef->varp()->user2p(), NodeModule); + } else { + classp = VN_AS(membersel->user2p(), NodeModule); + } + methodp->addPinsp(membersel); + } else { + classp = VN_AS(varp->user2p(), NodeModule); + AstVarRef* const varRefp + = new AstVarRef{varp->fileline(), classp, varp, VAccess::WRITE}; + varRefp->classOrPackagep(classOrPackagep); + methodp->addPinsp(varRefp); + } + AstNodeDType* tmpDtypep = varp->dtypep(); + while (VN_IS(tmpDtypep, UnpackArrayDType) || VN_IS(tmpDtypep, DynArrayDType) + || VN_IS(tmpDtypep, QueueDType) || VN_IS(tmpDtypep, AssocArrayDType)) + tmpDtypep = tmpDtypep->subDTypep(); + const size_t width = tmpDtypep->width(); + methodp->addPinsp( + new AstConst{varp->dtypep()->fileline(), AstConst::Unsized64{}, width}); + AstNodeExpr* const varnamep = new AstCExpr{varp->fileline(), AstCExpr::Pure{}, + "\"" + smtName + "\"", varp->width()}; + varnamep->dtypep(varp->dtypep()); + methodp->addPinsp(varnamep); + methodp->addPinsp( + new AstConst{varp->dtypep()->fileline(), AstConst::Unsized64{}, dimension}); + // Don't pass randMode.index for global constraints with membersel + if (randMode.usesMode && !(isGlobalConstrained && membersel)) { + methodp->addPinsp( + new AstConst{varp->fileline(), AstConst::Unsized64{}, randMode.index}); + } + AstNodeFTask* initTaskp = m_inlineInitTaskp; + if (!initTaskp) { + varp->user3(true); + if (membersel) { + initTaskp = VN_AS(m_memberMap.findMember(classp, "randomize"), NodeFTask); + UASSERT_OBJ(initTaskp, classp, "No randomize() in class"); + } else { + initTaskp = VN_AS(m_memberMap.findMember(classp, "new"), NodeFTask); + UASSERT_OBJ(initTaskp, classp, "No new() in class"); + } + } + initTaskp->addStmtsp(methodp->makeStmt()); } - initTaskp->addStmtsp(methodp->makeStmt()); } else { // Variable already written, clean up cloned membersel if any if (membersel) VL_DO_DANGLING(membersel->deleteTree(), membersel); @@ -1259,6 +1378,38 @@ class ConstraintExprVisitor final : public VNVisitor { nodep->replaceWith(varRefp); VL_DO_DANGLING(pushDeletep(nodep), nodep); visit(varRefp); + } else if (nodep->user1() && VN_IS(nodep->fromp(), CMethodHard) + && VN_AS(nodep->fromp(), CMethodHard)->method() == VCMethod::ARRAY_AT) { + // Class ref array element member access (e.g., items[i].val) + AstCMethodHard* const cmethodp = VN_AS(nodep->fromp(), CMethodHard); + AstNodeDType* const arrayDtp = cmethodp->fromp()->dtypep()->skipRefp(); + AstNodeDType* const elemDtp = arrayDtp->subDTypep()->skipRefp(); + if (VN_IS(elemDtp, ClassRefDType)) { + VL_RESTORER(m_structSel); + m_structSel = true; + iterateChildren(nodep); + FileLine* const fl = nodep->fileline(); + AstSFormatF* newp = nullptr; + if (AstSFormatF* const fromp = VN_CAST(nodep->fromp(), SFormatF)) { + if (fromp->name() == "%@.%@") { + newp = new AstSFormatF{fl, "%@.%@." + nodep->name(), false, + fromp->exprsp()->cloneTreePure(true)}; + } else { + newp = new AstSFormatF{fl, fromp->name() + "." + nodep->name(), false, + nullptr}; + } + } else { + newp = new AstSFormatF{fl, nodep->name(), false, nullptr}; + } + nodep->replaceWith(newp); + VL_DO_DANGLING(pushDeletep(nodep), nodep); + return; + } + // Not a class ref element, fall through to normal handling + iterateChildren(nodep); + nodep->replaceWith(nodep->fromp()->unlinkFrBack()); + VL_DO_DANGLING(nodep->deleteTree(), nodep); + return; } else if (nodep->user1()) { iterateChildren(nodep); nodep->replaceWith(nodep->fromp()->unlinkFrBack()); @@ -1270,6 +1421,7 @@ class ConstraintExprVisitor final : public VNVisitor { } void visit(AstSFormatF* nodep) override {} void visit(AstStmtExpr* nodep) override {} + void visit(AstCStmt* nodep) override {} void visit(AstConstraintIf* nodep) override { AstNodeExpr* newp = nullptr; FileLine* const fl = nodep->fileline(); diff --git a/test_regress/t/t_constraint_foreach_classref.py b/test_regress/t/t_constraint_foreach_classref.py new file mode 100755 index 000000000..9aaadb3d0 --- /dev/null +++ b/test_regress/t/t_constraint_foreach_classref.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# Copyright 2026 by Wilson Snyder. 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-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_foreach_classref.v b/test_regress/t/t_constraint_foreach_classref.v new file mode 100644 index 000000000..1200963c2 --- /dev/null +++ b/test_regress/t/t_constraint_foreach_classref.v @@ -0,0 +1,110 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain, for +// any use, without warranty, 2026 by PlanV GmbH. +// SPDX-License-Identifier: CC0-1.0 + +// Test: Inline foreach constraints on dynamic arrays and queues of class objects + +class Inner; + rand bit [7:0] val; + rand bit [3:0] tag; + + function new(); + val = 0; + tag = 0; + endfunction +endclass + +class OuterDyn; + rand Inner items[]; + + function new(int size = 3); + items = new[size]; + foreach (items[i]) items[i] = new(); + endfunction +endclass + +class OuterQueue; + rand Inner items[$]; + + function new(int size = 3); + Inner tmp; + for (int i = 0; i < size; i++) begin + tmp = new(); + items.push_back(tmp); + end + endfunction +endclass + +module t; + OuterDyn od; + OuterQueue oq; + + initial begin + // === Test 1: Dynamic array with inline foreach constraint === + od = new(3); + + if (od.randomize() with { + foreach (items[i]) { + items[i].val > 10; + items[i].val < 200; + items[i].tag > 0; + } + } == 0) begin + $display("FAIL: dyn randomize() returned 0"); + $stop; + end + + foreach (od.items[i]) begin + if (!(od.items[i].val > 10 && od.items[i].val < 200)) begin + $display("FAIL: dyn items[%0d].val=%0d out of range", i, od.items[i].val); + $stop; + end + if (od.items[i].tag == 0) begin + $display("FAIL: dyn items[%0d].tag=%0d should be > 0", i, od.items[i].tag); + $stop; + end + end + + // === Test 2: Empty dynamic array (should succeed trivially) === + od = new(0); + + if (od.randomize() with { + foreach (items[i]) { + items[i].val > 10; + } + } == 0) begin + $display("FAIL: empty dyn randomize() returned 0"); + $stop; + end + + // === Test 3: Queue with inline foreach constraint === + oq = new(3); + + if (oq.randomize() with { + foreach (items[i]) { + items[i].val > 50; + items[i].val < 150; + items[i].tag > 0; + } + } == 0) begin + $display("FAIL: queue randomize() returned 0"); + $stop; + end + + foreach (oq.items[i]) begin + if (!(oq.items[i].val > 50 && oq.items[i].val < 150)) begin + $display("FAIL: queue items[%0d].val=%0d out of range", i, oq.items[i].val); + $stop; + end + if (oq.items[i].tag == 0) begin + $display("FAIL: queue items[%0d].tag=%0d should be > 0", i, oq.items[i].tag); + $stop; + end + end + + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule