Fix Inline foreach constraints on dynamic arrays of class objects (#7030) (#7037)

This commit is contained in:
Yilou Wang 2026-02-11 00:22:31 +01:00 committed by GitHub
parent a031dd1a22
commit 2bb807a931
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 348 additions and 65 deletions

View File

@ -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<uint32_t, uint32_t> 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<uint64_t>(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<uint32_t, uint32_t> 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();

View File

@ -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()

View File

@ -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