Fix dist constraint on a frozen variable failing randomization (#7878)

This commit is contained in:
Yilou Wang 2026-07-21 12:39:09 +02:00 committed by GitHub
parent f22676eea9
commit b34f685f70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1293 additions and 12 deletions

View File

@ -3487,7 +3487,8 @@ class RandomizeVisitor final : public VNVisitor {
m_dynarrayDtp->dtypep(m_dynarrayDtp);
v3Global.rootp()->typeTablep()->addTypesp(m_dynarrayDtp);
}
AstVar* const modeVarp = new AstVar{fl, VVarType::MODULETEMP, name, m_dynarrayDtp};
// MEMBER, not MODULETEMP, so V3Localize does not localize it into new().
AstVar* const modeVarp = new AstVar{fl, VVarType::MEMBER, name, m_dynarrayDtp};
modeVarp->user2p(classp);
classp->addStmtsp(modeVarp);
return modeVarp;
@ -3508,6 +3509,9 @@ class RandomizeVisitor final : public VNVisitor {
// to point to the package scope when the variable is moved by V3Class.
modeVarp->user2p(classp);
classp->addStmtsp(modeVarp);
// Register so findStaticRandModeVarMember sees it through the already
// scanned member cache
m_memberMap.insert(classp, modeVarp);
return modeVarp;
}
static void addSetRandMode(AstNodeFTask* const ftaskp, AstVar* const genp,
@ -4627,11 +4631,16 @@ class RandomizeVisitor final : public VNVisitor {
}
static bool distBoundRefsRandVar(const AstNode* boundp) {
bool found = false;
boundp->foreach([&](const AstVarRef* vrefp) {
if (vrefp->varp()->rand().isRandomizable()) found = true;
return boundp->exists(
[](const AstVarRef* vrefp) { return vrefp->varp()->rand().isRandomizable(); });
}
static bool distBoundRefsModeVar(const AstNode* boundp) {
return boundp->exists([](const AstVarRef* vrefp) {
if (!vrefp->varp()->rand().isRandomizable()) return false;
const RandomizeMode rmode = {.asInt = vrefp->varp()->user1()};
return rmode.usesMode;
});
return found;
}
// (distExpr >= lo) && (distExpr <= hi); signed comparisons for signed vars
@ -4730,6 +4739,175 @@ class RandomizeVisitor final : public VNVisitor {
return new AstConstraintExpr{fl, unionExprp};
}
// Gate junction with constants folded and identical terms merged.
AstNodeExpr* newGateJoin(FileLine* fl, AstNodeExpr* ap, AstNodeExpr* bp, bool isAnd) {
if (ap->sameTree(bp)) {
VL_DO_DANGLING(pushDeletep(bp), bp);
return ap;
}
if (VN_IS(bp, Const)) std::swap(ap, bp);
if (VN_IS(ap, Const)) {
const bool constWins = VN_AS(ap, Const)->isZero() == isAnd;
if (!constWins) std::swap(ap, bp);
VL_DO_DANGLING(pushDeletep(bp), bp);
return ap;
}
if (isAnd) return new AstLogAnd{fl, ap, bp};
return new AstLogOr{fl, ap, bp};
}
AstNodeExpr* newGateOr(FileLine* fl, AstNodeExpr* ap, AstNodeExpr* bp) {
return newGateJoin(fl, ap, bp, false);
}
AstNodeExpr* newGateAnd(FileLine* fl, AstNodeExpr* ap, AstNodeExpr* bp) {
return newGateJoin(fl, ap, bp, true);
}
// The rand_mode bit of varp: static class var, owner object's array via the
// member select, or this object's array.
AstNodeExpr* newModeBitRead(AstVar* varp, const AstMemberSel* mselp, AstVar* randModeVarp,
FileLine* fl) {
AstNodeExpr* arrayp = nullptr;
if (varp->lifetime().isStatic()) {
AstClass* const ownerp = VN_CAST(varp->user2p(), Class);
UASSERT_OBJ(ownerp, varp, "usesMode static var without an owning class");
AstVar* const smodep = getStaticRandModeVar(ownerp);
UASSERT_OBJ(smodep, varp, "usesMode static var without a static rand-mode array");
arrayp = new AstVarRef{fl, VN_AS(smodep->user2p(), NodeModule), smodep, VAccess::READ};
} else if (mselp) {
AstVar* const memberModep = getRandModeVarFromClass(VN_AS(varp->user2p(), NodeModule));
UASSERT_OBJ(memberModep, varp, "usesMode member without a class rand-mode array");
arrayp = new AstMemberSel{fl, mselp->fromp()->cloneTreePure(false), memberModep};
} else {
UASSERT_OBJ(randModeVarp, varp, "rand_mode variable without a class mode array");
arrayp = new AstVarRef{fl, VN_AS(randModeVarp->user2p(), NodeModule), randModeVarp,
VAccess::READ};
}
const RandomizeMode rmode = {.asInt = varp->user1()};
AstCMethodHard* const atp
= new AstCMethodHard{fl, arrayp, VCMethod::ARRAY_AT, new AstConst{fl, rmode.index}};
atp->dtypeSetUInt32();
return atp;
}
// The rand_mode bit of one variable access, or a constant when it has none.
// A non-rand value can never be re-drawn; a non-rand owner level is skipped.
AstNodeExpr* newVarGate(AstVar* varp, const AstMemberSel* mselp, bool ownerLevel,
AstVar* randModeVarp, FileLine* fl) {
if (!varp->rand().isRandomizable()) {
return new AstConst{fl, AstConst::BitTrue{}, ownerLevel};
}
const RandomizeMode rmode = {.asInt = varp->user1()};
if (!rmode.usesMode) return new AstConst{fl, AstConst::BitTrue{}};
return newModeBitRead(varp, mselp, randModeVarp, fl);
}
// Change bit of one variable access, ANDed level by level down the
// member-select owner path.
AstNodeExpr* newAccessGate(const AstNode* nodep, bool ownerLevel, AstVar* randModeVarp,
FileLine* fl) {
if (const AstMemberSel* const mselp = VN_CAST(nodep, MemberSel)) {
return newGateAnd(fl, newVarGate(mselp->varp(), mselp, ownerLevel, randModeVarp, fl),
newAccessGate(mselp->fromp(), true, randModeVarp, fl));
}
if (const AstNodeVarRef* const refp = VN_CAST(nodep, NodeVarRef)) {
return newVarGate(refp->varp(), nullptr, ownerLevel, randModeVarp, fl);
}
if (const AstNodeSel* const selp = VN_CAST(nodep, NodeSel)) {
return newAccessGate(selp->fromp(), true, randModeVarp, fl);
}
if (distBoundRefsModeVar(nodep)) {
nodep->v3warn(E_UNSUPPORTED,
"Unsupported: 'rand_mode' on a variable used inside this form of"
" 'dist' expression");
}
return new AstConst{fl, AstConst::BitTrue{}};
}
// Gate of one dist expression node: nonzero while its value can still be
// freshly drawn. Constant when no runtime rand_mode bit is involved.
AstNodeExpr* newDistGate(const AstNode* nodep, AstVar* randModeVarp, FileLine* fl) {
if (VN_IS(nodep, NodeVarRef) || VN_IS(nodep, MemberSel)) {
return newAccessGate(nodep, false, randModeVarp, fl);
}
// A selection is drawn through its base; the index never gates.
if (const AstNodeSel* const selp = VN_CAST(nodep, NodeSel)) {
return newDistGate(selp->fromp(), randModeVarp, fl);
}
if (const AstSel* const selp = VN_CAST(nodep, Sel)) {
return newDistGate(selp->fromp(), randModeVarp, fl);
}
// A conditional re-draws through an active selector or the selected arm.
if (const AstCond* const condp = VN_CAST(nodep, Cond)) {
AstNodeExpr* const thenGatep = newDistGate(condp->thenp(), randModeVarp, fl);
AstNodeExpr* elseGatep = newDistGate(condp->elsep(), randModeVarp, fl);
AstNodeExpr* armGatep = thenGatep;
if (thenGatep->sameTree(elseGatep)) {
VL_DO_DANGLING(pushDeletep(elseGatep), elseGatep);
} else {
armGatep
= new AstCond{fl, condp->condp()->cloneTreePure(false), thenGatep, elseGatep};
}
return newGateOr(fl, newDistGate(condp->condp(), randModeVarp, fl), armGatep);
}
if (const AstNodeUniop* const uopp = VN_CAST(nodep, NodeUniop)) {
return newDistGate(uopp->lhsp(), randModeVarp, fl);
}
if (const AstNodeBiop* const bopp = VN_CAST(nodep, NodeBiop)) {
return newGateOr(fl, newDistGate(bopp->lhsp(), randModeVarp, fl),
newDistGate(bopp->rhsp(), randModeVarp, fl));
}
if (distBoundRefsModeVar(nodep)) {
nodep->v3warn(E_UNSUPPORTED,
"Unsupported: 'rand_mode' on a variable used inside this form of"
" 'dist' expression");
}
// Constants and remaining forms re-draw only if a rand var is inside.
return new AstConst{fl, AstConst::BitTrue{}, distBoundRefsRandVar(nodep)};
}
// Membership union of the dist set, with runtime-zero weights excluded.
AstConstraintExpr* buildFrozenMembership(AstDist* distp,
const std::vector<DistBucket>& buckets) {
FileLine* const fl = distp->fileline();
AstNodeExpr* unionExprp = nullptr;
for (const auto& bucket : buckets) {
AstNodeExpr* termp = newDistMembershipTerm(distp, bucket.rangep);
// A weight that is zero only at runtime excludes the bucket's values too.
const bool runtimeWeight
= bucket.weightExprp->exists([](const AstNodeVarRef*) { return true; });
if (runtimeWeight) {
AstNeq* const nzp = new AstNeq{fl, bucket.weightExprp->cloneTreePure(false),
new AstConst{fl, AstConst::Unsized64{}, 0}};
nzp->user1(true);
termp = new AstLogAnd{fl, termp, nzp};
termp->user1(true);
}
if (unionExprp) {
unionExprp = new AstLogOr{fl, unionExprp, termp};
unionExprp->user1(true);
} else {
unionExprp = termp;
}
}
return new AstConstraintExpr{fl, unionExprp};
}
// Weighted chain while the dist value can still be freshly drawn, membership
// once it is frozen; membership only when it can never be re-drawn.
AstNode* gateFrozenDist(AstNode* chainp, AstDist* distp,
const std::vector<DistBucket>& buckets, AstVar* randModeVarp) {
FileLine* const fl = distp->fileline();
AstNodeExpr* gatep = newDistGate(distp->exprp(), randModeVarp, fl);
if (VN_IS(gatep, Const)) {
const bool drawable = !VN_AS(gatep, Const)->isZero();
VL_DO_DANGLING(pushDeletep(gatep), gatep);
if (drawable) return chainp;
VL_DO_DANGLING(pushDeletep(chainp), chainp);
return buildFrozenMembership(distp, buckets);
}
return new AstConstraintIf{fl, gatep, chainp, buildFrozenMembership(distp, buckets)};
}
AstNodeExpr* newUniformRangePick(AstDist* distp, const AstInsideRange* irp) {
FileLine* const fl = distp->fileline();
AstNodeExpr* const distExprCopyp = distp->exprp()->cloneTreePure(false);
@ -4801,7 +4979,7 @@ class RandomizeVisitor final : public VNVisitor {
// Replace AstDist with weighted bucket selection via AstConstraintIf chain.
// Supports both constant and variable weight expressions.
void lowerDistConstraints(AstTask* taskp, AstNode* constrItemsp,
void lowerDistConstraints(AstTask* taskp, AstNode* constrItemsp, AstVar* randModeVarp,
AstConstraintForeach* foreachp = nullptr) {
// When inside a foreach, bucket preamble stmts are stored in foreachp->user3p()
// (as a linked list) so visit(AstConstraintForeach*) can inject them into the
@ -4827,13 +5005,15 @@ class RandomizeVisitor final : public VNVisitor {
// dist can appear inside an if/else or foreach constraint.
if (AstConstraintIf* const cifp = VN_CAST(itemp, ConstraintIf)) {
UASSERT_OBJ(cifp->thensp(), cifp, "constraint if without a then body");
lowerDistConstraints(taskp, cifp->thensp(), foreachp);
if (cifp->elsesp()) { lowerDistConstraints(taskp, cifp->elsesp(), foreachp); }
lowerDistConstraints(taskp, cifp->thensp(), randModeVarp, foreachp);
if (cifp->elsesp()) {
lowerDistConstraints(taskp, cifp->elsesp(), randModeVarp, foreachp);
}
continue;
}
if (AstConstraintForeach* const cfep = VN_CAST(itemp, ConstraintForeach)) {
UASSERT_OBJ(cfep->bodyp(), cfep, "constraint foreach without a body");
lowerDistConstraints(taskp, cfep->bodyp(), cfep);
lowerDistConstraints(taskp, cfep->bodyp(), randModeVarp, cfep);
continue;
}
@ -4853,7 +5033,7 @@ class RandomizeVisitor final : public VNVisitor {
AstConstraintIf* const liftedp = liftLogIfChainToConstraintIf(topLogIfp);
constrExprp->replaceWith(liftedp);
VL_DO_DANGLING(pushDeletep(constrExprp), constrExprp);
lowerDistConstraints(taskp, liftedp->thensp(), foreachp);
lowerDistConstraints(taskp, liftedp->thensp(), randModeVarp, foreachp);
continue;
}
}
@ -4933,7 +5113,10 @@ class RandomizeVisitor final : public VNVisitor {
cumSums.push_back(runningSump->cloneTreePure(true));
}
AstNode* const chainp = buildWeightedBucketChain(distp, buckets, bucketVarp, cumSums);
AstNode* chainp = buildWeightedBucketChain(distp, buckets, bucketVarp, cumSums);
chainp = gateFrozenDist(chainp, distp, buckets, randModeVarp);
constrExprp->replaceWith(chainp);
VL_DO_DANGLING(pushDeletep(constrExprp), constrExprp);
// Hard membership precedes the soft bucket chain in the constraint list.
@ -5026,7 +5209,9 @@ class RandomizeVisitor final : public VNVisitor {
}
if (constrp->itemsp()) expandUniqueElementList(constrp->itemsp());
if (constrp->itemsp()) lowerDistConstraints(taskp, constrp->itemsp());
if (constrp->itemsp()) {
lowerDistConstraints(taskp, constrp->itemsp(), randModeVarp);
}
std::set<AstVar*>& sizeArrays = m_sizeConstrainedArrays[classp];
ConstraintExprVisitor{classp, m_memberMap, constrp->itemsp(),
nullptr, genp, randModeVarp,

View File

@ -0,0 +1,17 @@
#!/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')
# Emitted C++ for call-owner constraints does not yet compile; verilate only
test.compile(verilator_make_gmake=False)
test.passes()

View File

@ -0,0 +1,34 @@
// 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
class Inner;
rand int x;
endclass
class Outer;
rand Inner m;
function Inner get();
return m;
endfunction
constraint c {
get().x dist {
5 := 9,
10 := 1
};
}
function new();
m = new;
endfunction
endclass
module t;
initial begin
automatic Outer o = new;
automatic int ok = o.randomize();
$display("ok=%0d x=%0d", ok, o.m.x);
$finish;
end
endmodule

View File

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

View File

@ -0,0 +1,949 @@
// 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
// verilog_format: off
`define stop $stop
`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0);
// verilog_format: on
// A frozen dist variable degrades to a membership test (IEEE 1800-2023 18.5.3).
typedef int unsigned uint;
class Base;
rand uint delay;
rand uint x;
constraint delay_c {delay inside {[1 : 100]};}
function int rd();
return this.randomize(delay);
endfunction
endclass
class SingleVals extends Base;
constraint c {
x dist {
5 := 99,
1000000 := 1
};
}
endclass
class RangeVals extends Base;
constraint c {
x dist {
[1 : 10] := 1,
[1000 : 2000] := 9
};
}
endclass
class MixVals extends Base;
constraint c {
x dist {
0 := 1,
[100 : 200] := 5,
9999 := 3
};
}
endclass
class DistInIf extends Base;
rand uint sel;
constraint c {
sel inside {0, 1};
if (sel == 1)
x dist {
5 := 9,
70 := 1
};
else
x == 4242;
}
endclass
class DistInForeach;
rand uint arr[4];
constraint c {
foreach (arr[i])
arr[i] dist {
10 := 9,
20 := 1
};
}
endclass
class StaticVar;
rand uint delay;
static rand uint sx;
constraint delay_c {delay inside {[1 : 100]};}
constraint c {
sx dist {
5 := 9,
70 := 1
};
}
function int rd();
return this.randomize(delay);
endfunction
endclass
class StaticPlain; // non-dist constraint on a frozen static var
rand uint d;
static rand uint sy;
constraint c {sy inside {[10 : 20]};}
endclass
class Sub;
rand uint x;
endclass
class Holder;
rand Sub s;
rand uint delay;
constraint delay_c {delay inside {[1 : 100]};}
constraint c {
s.x dist {
5 := 9,
70 := 1
};
}
function new();
s = new;
endfunction
endclass
class MultiVar;
rand uint x, y;
constraint c {
(x + y) dist {
10 := 9,
1000 := 1
};
}
endclass
class DupVar;
rand uint x;
constraint c {
(x + x) dist {
10 := 9,
20 := 1
};
}
endclass
class DupMember;
rand Sub s;
constraint c {
(s.x + s.x) dist {
10 := 9,
20 := 1
};
}
function new();
s = new;
endfunction
endclass
class RtWeight;
rand uint x;
uint w;
constraint c {
x dist {
5 := 9,
70 := w
};
}
endclass
class IdxSel;
rand uint a[4];
rand uint idx;
constraint ci {idx inside {[0 : 3]};}
constraint c {
a[idx] dist {
10 := 9,
20 := 1
};
}
endclass
class ConstOp;
rand uint x;
constraint c {
(x + 1) dist {
6 := 9,
71 := 1
};
}
endclass
class StateMix;
rand uint x;
uint nw;
constraint c {
(x + nw) dist {
5 := 9,
70 := 1
};
}
endclass
class MixedFreeze;
rand uint x, z;
constraint c {
(x + z) dist {
10 := 9,
1000 := 1
};
}
endclass
class NoModeHandle;
rand Sub s2;
constraint c {
s2.x dist {
5 := 9,
70 := 1
};
}
function new();
s2 = new;
endfunction
endclass
class SelOp;
rand uint x;
constraint c {
x[15:0] dist {
16'd5 := 9,
16'd70 := 1
};
}
endclass
class NotOp;
rand uint x;
constraint c {
(~x) dist {
32'hFFFF_FFFA := 9,
32'hFFFF_FFB9 := 1
};
}
endclass
class CondSel;
rand uint x, y;
rand bit b;
constraint c {
(b ? x : y) dist {
5 := 9,
70 := 1
};
}
endclass
class ObjArr;
rand Sub arr2[2];
constraint c {
arr2[0].x dist {
5 := 9,
70 := 1
};
}
function new();
arr2[0] = new;
arr2[1] = new;
endfunction
endclass
// Same SV shape as CondSel, but x2/y2 never use rand_mode anywhere in this
// test: the arm gates fold to a constant at verilation instead of runtime bits.
class CondSame;
rand uint x2, y2;
rand bit b2;
constraint c {
(b2 ? x2 : y2) dist {
5 := 9,
70 := 1
};
}
endclass
typedef struct packed {
bit [15:0] a;
bit [15:0] b;
} ab_t;
class PackedStruct;
rand ab_t st;
constraint c {
st.a dist {
16'd5 := 9,
16'd70 := 1
};
}
endclass
class Mixed;
rand uint d;
uint nf;
endclass
class NonRandMember;
rand Mixed m;
constraint c {
m.nf dist {
5 := 9,
70 := 1
};
}
function new();
m = new;
endfunction
endclass
class QSel;
rand Sub q[$];
int pick;
constraint c {
q[pick].x dist {
30 := 9,
90 := 1
};
}
function new();
Sub tmp = new;
q.push_back(tmp);
endfunction
endclass
module t;
int ok;
initial begin
SingleVals sv;
RangeVals rv;
MixVals mv;
StaticVar st;
Holder h;
MultiVar m;
RtWeight r;
DistInIf di;
DistInForeach df;
IdxSel ix;
// Frozen in the low-weight bucket -> succeeds, value untouched.
sv = new;
ok = sv.randomize();
`checkd(ok, 1);
sv.x.rand_mode(0);
sv.x = 1000000;
for (int i = 0; i < 8; ++i) begin
ok = sv.rd();
`checkd(ok, 1);
`checkd(sv.x, 1000000);
if (sv.delay < 1 || sv.delay > 100) $stop;
end
// Frozen in the high-weight bucket -> succeeds.
sv = new;
ok = sv.randomize();
`checkd(ok, 1);
sv.x.rand_mode(0);
sv.x = 5;
for (int i = 0; i < 8; ++i) begin
ok = sv.rd();
`checkd(ok, 1);
`checkd(sv.x, 5);
end
// Frozen OUTSIDE the set -> membership violated, randomize fails every call.
sv = new;
ok = sv.randomize();
`checkd(ok, 1);
sv.x.rand_mode(0);
sv.x = 7;
for (int i = 0; i < 8; ++i) begin
ok = sv.rd();
`checkd(ok, 0);
`checkd(sv.x, 7);
end
// Setting rand_mode back to 1 restores drawing.
sv.x.rand_mode(1);
for (int i = 0; i < 8; ++i) begin
ok = sv.randomize();
`checkd(ok, 1);
if (sv.x != 5 && sv.x != 1000000) $stop;
end
// Frozen inside a range bucket -> succeeds.
rv = new;
ok = rv.randomize();
`checkd(ok, 1);
rv.x.rand_mode(0);
rv.x = 5;
for (int i = 0; i < 8; ++i) begin
ok = rv.rd();
`checkd(ok, 1);
`checkd(rv.x, 5);
end
// Frozen inside a mixed single+range set -> succeeds.
mv = new;
ok = mv.randomize();
`checkd(ok, 1);
mv.x.rand_mode(0);
mv.x = 150;
for (int i = 0; i < 8; ++i) begin
ok = mv.rd();
`checkd(ok, 1);
`checkd(mv.x, 150);
end
// Active dist var still solves weighted toward the heavy bucket.
sv = new;
begin
automatic int n5 = 0, nbig = 0;
for (int i = 0; i < 64; ++i) begin
ok = sv.randomize();
`checkd(ok, 1);
if (sv.x == 5) n5++;
else if (sv.x == 1000000) nbig++;
else $stop;
end
if (n5 <= nbig) $stop;
end
// Static rand var frozen in the low-weight bucket -> succeeds.
st = new;
ok = st.randomize();
`checkd(ok, 1);
st.sx.rand_mode(0);
st.sx = 70;
for (int i = 0; i < 8; ++i) begin
ok = st.rd();
`checkd(ok, 1);
`checkd(st.sx, 70);
ok = st.randomize();
`checkd(ok, 1);
`checkd(st.sx, 70);
end
// Static rand var frozen outside the set -> fails every call.
st.sx = 7;
for (int i = 0; i < 8; ++i) begin
ok = st.randomize();
`checkd(ok, 0);
`checkd(st.sx, 7);
end
// Unfreezing the static var restores drawing.
st.sx.rand_mode(1);
for (int i = 0; i < 8; ++i) begin
ok = st.randomize();
`checkd(ok, 1);
if (st.sx != 5 && st.sx != 70) $stop;
end
// Frozen static var under a plain constraint: current value decides.
begin
StaticPlain sp;
sp = new;
ok = sp.randomize();
`checkd(ok, 1);
sp.sy.rand_mode(0);
sp.sy = 15;
for (int i = 0; i < 8; ++i) begin
ok = sp.randomize();
`checkd(ok, 1);
`checkd(sp.sy, 15);
end
sp.sy = 99;
for (int i = 0; i < 8; ++i) begin
ok = sp.randomize();
`checkd(ok, 0);
`checkd(sp.sy, 99);
end
end
// Sub-object member frozen in the low-weight bucket -> succeeds.
h = new;
ok = h.randomize();
`checkd(ok, 1);
h.s.x.rand_mode(0);
h.s.x = 70;
for (int i = 0; i < 8; ++i) begin
ok = h.randomize();
`checkd(ok, 1);
`checkd(h.s.x, 70);
end
// Sub-object member frozen outside the set -> fails every call.
h.s.x = 7;
for (int i = 0; i < 8; ++i) begin
ok = h.randomize();
`checkd(ok, 0);
`checkd(h.s.x, 7);
end
// Unfreezing the sub-object member restores drawing.
h.s.x.rand_mode(1);
for (int i = 0; i < 8; ++i) begin
ok = h.randomize();
`checkd(ok, 1);
if (h.s.x != 5 && h.s.x != 70) $stop;
end
// Both the handle and the member frozen.
h = new;
ok = h.randomize();
`checkd(ok, 1);
h.s.rand_mode(0);
h.s.x.rand_mode(0);
h.s.x = 5;
for (int i = 0; i < 8; ++i) begin
ok = h.randomize();
`checkd(ok, 1);
`checkd(h.s.x, 5);
end
h.s.x = 7;
for (int i = 0; i < 8; ++i) begin
ok = h.randomize();
`checkd(ok, 0);
`checkd(h.s.x, 7);
end
// Multi-var dist expression, one var frozen: the active var must still
// satisfy membership (sum wraps mod 2**32, so both values stay reachable).
m = new;
ok = m.randomize();
`checkd(ok, 1);
m.x.rand_mode(0);
m.x = 600;
for (int i = 0; i < 8; ++i) begin
ok = m.randomize();
`checkd(ok, 1);
`checkd(m.x, 600);
if (m.x + m.y != 10 && m.x + m.y != 1000) $stop;
end
// Both vars frozen: membership of the frozen sum decides.
m.y.rand_mode(0);
m.y = 400;
ok = m.randomize();
`checkd(ok, 1);
m.y = 500;
for (int i = 0; i < 8; ++i) begin
ok = m.randomize();
`checkd(ok, 0);
`checkd(m.y, 500);
end
// Same var twice in the dist expression, frozen: membership of 2*x decides.
begin
DupVar d;
d = new;
ok = d.randomize();
`checkd(ok, 1);
d.x.rand_mode(0);
d.x = 5;
for (int i = 0; i < 8; ++i) begin
ok = d.randomize();
`checkd(ok, 1);
`checkd(d.x, 5);
end
d.x = 7;
for (int i = 0; i < 8; ++i) begin
ok = d.randomize();
`checkd(ok, 0);
`checkd(d.x, 7);
end
end
// Same sub-object member twice in the dist expression, frozen.
begin
DupMember dm;
dm = new;
ok = dm.randomize();
`checkd(ok, 1);
dm.s.x.rand_mode(0);
dm.s.x = 5;
for (int i = 0; i < 8; ++i) begin
ok = dm.randomize();
`checkd(ok, 1);
`checkd(dm.s.x, 5);
end
dm.s.x = 7;
for (int i = 0; i < 8; ++i) begin
ok = dm.randomize();
`checkd(ok, 0);
`checkd(dm.s.x, 7);
end
end
// Runtime weight of zero excludes the bucket's values (18.5.3): frozen
// there must fail; nonzero weight on the same frozen value succeeds.
r = new;
r.w = 1;
ok = r.randomize();
`checkd(ok, 1);
r.x.rand_mode(0);
r.x = 70;
for (int i = 0; i < 8; ++i) begin
ok = r.randomize();
`checkd(ok, 1);
`checkd(r.x, 70);
end
r.w = 0;
for (int i = 0; i < 8; ++i) begin
ok = r.randomize();
`checkd(ok, 0);
`checkd(r.x, 70);
end
// Active var with a runtime-zero weight never draws that bucket.
r = new;
r.w = 0;
for (int i = 0; i < 8; ++i) begin
ok = r.randomize();
`checkd(ok, 1);
`checkd(r.x, 5);
end
// dist inside a constraint if: taken branch follows the dist, else pins 4242.
di = new;
for (int i = 0; i < 16; ++i) begin
ok = di.randomize();
`checkd(ok, 1);
if (di.sel == 1) begin
if (di.x != 5 && di.x != 70) $stop;
end
else begin
`checkd(di.x, 4242);
end
end
// Frozen x forces the solver to whichever branch its value satisfies.
di.x.rand_mode(0);
di.x = 70;
for (int i = 0; i < 8; ++i) begin
ok = di.randomize();
`checkd(ok, 1);
`checkd(di.sel, 1);
`checkd(di.x, 70);
end
di.x = 4242;
for (int i = 0; i < 8; ++i) begin
ok = di.randomize();
`checkd(ok, 1);
`checkd(di.sel, 0);
`checkd(di.x, 4242);
end
di.x = 7;
for (int i = 0; i < 8; ++i) begin
ok = di.randomize();
`checkd(ok, 0);
`checkd(di.x, 7);
end
// dist inside a constraint foreach: every element honors the set.
df = new;
for (int i = 0; i < 16; ++i) begin
ok = df.randomize();
`checkd(ok, 1);
foreach (df.arr[j]) if (df.arr[j] != 10 && df.arr[j] != 20) $stop;
end
// Frozen index into an active array: a[idx] still draws BOTH values,
// biased toward the heavy bucket.
ix = new;
ok = ix.randomize();
`checkd(ok, 1);
ix.idx.rand_mode(0);
ix.idx = 2;
begin
automatic int n10 = 0, n20 = 0;
for (int i = 0; i < 64; ++i) begin
ok = ix.randomize();
`checkd(ok, 1);
`checkd(ix.idx, 2);
if (ix.a[2] == 10) n10++;
else if (ix.a[2] == 20) n20++;
else $stop;
end
if (n10 == 0 || n20 == 0) $stop;
if (n10 <= n20) $stop;
end
// Constant operand: frozen (x + 1) follows membership of the sum.
begin
ConstOp cp;
cp = new;
ok = cp.randomize();
`checkd(ok, 1);
if (cp.x != 5 && cp.x != 70) $stop;
cp.x.rand_mode(0);
cp.x = 70;
for (int i = 0; i < 8; ++i) begin
ok = cp.randomize();
`checkd(ok, 1);
`checkd(cp.x, 70);
end
cp.x = 7;
for (int i = 0; i < 8; ++i) begin
ok = cp.randomize();
`checkd(ok, 0);
`checkd(cp.x, 7);
end
end
// Non-rand term in the dist expression: only the rand var gates, and the
// state value participates in the frozen membership.
begin
StateMix sm;
sm = new;
sm.nw = 0;
ok = sm.randomize();
`checkd(ok, 1);
if (sm.x != 5 && sm.x != 70) $stop;
sm.x.rand_mode(0);
sm.x = 70;
for (int i = 0; i < 8; ++i) begin
ok = sm.randomize();
`checkd(ok, 1);
`checkd(sm.x, 70);
end
sm.x = 7;
for (int i = 0; i < 8; ++i) begin
ok = sm.randomize();
`checkd(ok, 0);
`checkd(sm.x, 7);
end
sm.nw = 65;
sm.x = 5;
ok = sm.randomize();
`checkd(ok, 1);
`checkd(sm.x, 5);
sm.nw = 66;
ok = sm.randomize();
`checkd(ok, 0);
`checkd(sm.x, 5);
end
// Partner var never uses rand_mode: freezing x leaves the dist weighted.
begin
MixedFreeze mf;
mf = new;
ok = mf.randomize();
`checkd(ok, 1);
mf.x.rand_mode(0);
mf.x = 600;
begin
automatic int n10 = 0, n1000 = 0;
for (int i = 0; i < 64; ++i) begin
ok = mf.randomize();
`checkd(ok, 1);
`checkd(mf.x, 600);
if (mf.x + mf.z == 10) n10++;
else if (mf.x + mf.z == 1000) n1000++;
else $stop;
end
if (n10 == 0 || n1000 == 0) $stop;
if (n10 <= n1000) $stop;
end
end
// Handle without rand_mode in the chain: the member's own bit still gates.
begin
NoModeHandle nm;
nm = new;
ok = nm.randomize();
`checkd(ok, 1);
nm.s2.x.rand_mode(0);
nm.s2.x = 70;
for (int i = 0; i < 8; ++i) begin
ok = nm.randomize();
`checkd(ok, 1);
`checkd(nm.s2.x, 70);
end
nm.s2.x = 7;
for (int i = 0; i < 8; ++i) begin
ok = nm.randomize();
`checkd(ok, 0);
`checkd(nm.s2.x, 7);
end
end
// Bit-select: frozen x degrades to membership of the selection.
begin
SelOp se;
se = new;
ok = se.randomize();
`checkd(ok, 1);
if (se.x[15:0] != 5 && se.x[15:0] != 70) $stop;
se.x.rand_mode(0);
se.x = 70;
for (int i = 0; i < 8; ++i) begin
ok = se.randomize();
`checkd(ok, 1);
`checkd(se.x, 70);
end
se.x = 7;
for (int i = 0; i < 8; ++i) begin
ok = se.randomize();
`checkd(ok, 0);
`checkd(se.x, 7);
end
end
// Unary operand: frozen (~x) follows membership of the inverted value.
begin
NotOp nt;
nt = new;
ok = nt.randomize();
`checkd(ok, 1);
if (nt.x != 5 && nt.x != 70) $stop;
nt.x.rand_mode(0);
nt.x = 5;
for (int i = 0; i < 8; ++i) begin
ok = nt.randomize();
`checkd(ok, 1);
`checkd(nt.x, 5);
end
nt.x = 7;
for (int i = 0; i < 8; ++i) begin
ok = nt.randomize();
`checkd(ok, 0);
`checkd(nt.x, 7);
end
end
// Conditional: frozen selector picks the live arm; a frozen selected arm
// degrades to membership of the selected value.
begin
CondSel cs;
cs = new;
for (int i = 0; i < 8; ++i) begin
ok = cs.randomize();
`checkd(ok, 1);
if ((cs.b ? cs.x : cs.y) != 5 && (cs.b ? cs.x : cs.y) != 70) $stop;
end
cs.b.rand_mode(0);
cs.b = 1'b1;
begin
automatic int n5 = 0, n70 = 0;
uint y0;
automatic bit yvar = 0;
ok = cs.randomize();
`checkd(ok, 1);
y0 = cs.y;
for (int i = 0; i < 64; ++i) begin
ok = cs.randomize();
`checkd(ok, 1);
`checkd(cs.b, 1'b1);
if (cs.x == 5) n5++;
else if (cs.x == 70) n70++;
else $stop;
if (cs.y != y0) yvar = 1;
end
if (n5 == 0 || n70 == 0) $stop;
if (n5 <= n70) $stop;
if (!yvar) $stop;
end
cs.x.rand_mode(0);
cs.x = 70;
for (int i = 0; i < 8; ++i) begin
ok = cs.randomize();
`checkd(ok, 1);
`checkd(cs.x, 70);
end
cs.x = 7;
for (int i = 0; i < 8; ++i) begin
ok = cs.randomize();
`checkd(ok, 0);
`checkd(cs.x, 7);
end
end
// Conditional with mode-less arms: a frozen selector alone stays weighted.
begin
CondSame cq;
cq = new;
ok = cq.randomize();
`checkd(ok, 1);
cq.b2.rand_mode(0);
for (int i = 0; i < 8; ++i) begin
ok = cq.randomize();
`checkd(ok, 1);
if ((cq.b2 ? cq.x2 : cq.y2) != 5 && (cq.b2 ? cq.x2 : cq.y2) != 70) $stop;
end
end
// Object-array element dist honors the set.
begin
ObjArr oa;
oa = new;
for (int i = 0; i < 8; ++i) begin
ok = oa.randomize();
`checkd(ok, 1);
if (oa.arr2[0].x != 5 && oa.arr2[0].x != 70) $stop;
end
end
// Packed struct member: frozen struct follows membership of the field.
begin
PackedStruct ps;
ps = new;
ok = ps.randomize();
`checkd(ok, 1);
if (ps.st.a != 5 && ps.st.a != 70) $stop;
ps.st.rand_mode(0);
ps.st = '{a: 16'd70, b: 16'd0};
for (int i = 0; i < 8; ++i) begin
ok = ps.randomize();
`checkd(ok, 1);
`checkd(ps.st.a, 70);
end
ps.st = '{a: 16'd7, b: 16'd0};
for (int i = 0; i < 8; ++i) begin
ok = ps.randomize();
`checkd(ok, 0);
`checkd(ps.st.a, 7);
end
end
// Dist on a non-rand member of a rand object: in-set value succeeds.
begin
NonRandMember nr;
nr = new;
nr.m.nf = 5;
ok = nr.randomize();
`checkd(ok, 1);
`checkd(nr.m.nf, 5);
end
// Queue element member: draws both values, biased toward the heavy bucket.
begin
QSel qs;
qs = new;
begin
automatic int n30 = 0, n90 = 0;
for (int i = 0; i < 64; ++i) begin
ok = qs.randomize();
`checkd(ok, 1);
if (qs.q[0].x == 30) n30++;
else if (qs.q[0].x == 90) n90++;
else $stop;
end
if (n30 == 0 || n90 == 0) $stop;
if (n30 <= n90) $stop;
end
end
$write("*-* All Finished *-*\n");
$finish;
end
endmodule

View File

@ -0,0 +1,10 @@
%Error-UNSUPPORTED: t/t_constraint_dist_randmode_unsup.v:14:6: Unsupported: 'rand_mode' on a variable used inside this form of 'dist' expression
: ... note: In instance 't'
14 | q[0].x dist {
| ^
... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest
%Error-UNSUPPORTED: t/t_constraint_dist_randmode_unsup.v:28:7: Unsupported: 'rand_mode' on a variable used inside this form of 'dist' expression
: ... note: In instance 't'
28 | iq[0] dist {
| ^
%Error: Exiting due to

View File

@ -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('simulator')
test.compile(fails=True, expect_filename=test.golden_filename)
test.passes()

View File

@ -0,0 +1,49 @@
// 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
class Inner;
rand int x;
endclass
class QOwner;
rand Inner q[$];
constraint c {
q[0].x dist {
5 := 9,
10 := 1
};
}
function new();
Inner tmp = new;
q.push_back(tmp);
endfunction
endclass
class QValue;
rand int iq[$];
constraint c {
iq[0] dist {
5 := 9,
10 := 1
};
}
function new();
iq.push_back(1);
endfunction
endclass
module t;
initial begin
automatic QOwner qo = new;
automatic QValue qv = new;
automatic int ok;
qo.q.rand_mode(0);
qv.iq.rand_mode(0);
ok = qo.randomize();
ok = qv.randomize();
$finish;
end
endmodule