Fix dist constraint on a frozen variable failing randomization

This commit is contained in:
Yilou Wang 2026-07-04 10:48:22 +02:00
parent d84af81a11
commit 06907c5358
3 changed files with 423 additions and 7 deletions

View File

@ -3269,7 +3269,10 @@ 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: V3Localize exempts class members; a temp whose
// only VarRefs sit in new() would be localized there, leaving the member
// read through member selects forever empty (= all inactive)
AstVar* const modeVarp = new AstVar{fl, VVarType::MEMBER, name, m_dynarrayDtp};
modeVarp->user2p(classp);
classp->addStmtsp(modeVarp);
return modeVarp;
@ -3290,6 +3293,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,
@ -4331,20 +4337,20 @@ 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) {
for (AstNode *nextip, *itemp = constrItemsp; itemp; itemp = nextip) {
nextip = itemp->nextp();
// Recursively handle ConstraintIf nodes (dist can be inside if/else)
if (AstConstraintIf* const cifp = VN_CAST(itemp, ConstraintIf)) {
if (cifp->thensp()) lowerDistConstraints(taskp, cifp->thensp());
if (cifp->elsesp()) lowerDistConstraints(taskp, cifp->elsesp());
if (cifp->thensp()) lowerDistConstraints(taskp, cifp->thensp(), randModeVarp);
if (cifp->elsesp()) lowerDistConstraints(taskp, cifp->elsesp(), randModeVarp);
continue;
}
// Recursively handle ConstraintForeach nodes (dist can be inside foreach)
if (AstConstraintForeach* const cfep = VN_CAST(itemp, ConstraintForeach)) {
if (cfep->bodyp()) lowerDistConstraints(taskp, cfep->bodyp());
if (cfep->bodyp()) lowerDistConstraints(taskp, cfep->bodyp(), randModeVarp);
continue;
}
@ -4364,7 +4370,7 @@ class RandomizeVisitor final : public VNVisitor {
AstConstraintIf* const liftedp = liftLogIfChainToConstraintIf(topLogIfp);
constrExprp->replaceWith(liftedp);
VL_DO_DANGLING(pushDeletep(constrExprp), constrExprp);
lowerDistConstraints(taskp, liftedp->thensp());
lowerDistConstraints(taskp, liftedp->thensp(), randModeVarp);
continue;
}
}
@ -4519,6 +4525,113 @@ class RandomizeVisitor final : public VNVisitor {
}
}
// The bucket chain re-draws a bucket every randomize() call. If a
// mode-carrying variable of the dist expression is INACTIVE at that
// call (rand_mode(0) / not listed in a scoped randomize), its frozen
// value stays as-is and asserting the freshly drawn bucket is wrong:
// whenever the frozen value lies in a different bucket the constraint
// set is UNSAT and randomize() fails. Gate the chain on the
// conjunction of the runtime rand_mode bits of every referenced
// variable (direct, static, or sub-object member); when any is frozen
// require only membership in the union of the dist ranges, excluding
// runtime-zero weights (IEEE 1800-2023 18.5.3).
AstNodeExpr* gatep = nullptr;
std::unordered_set<const AstVar*> gatedVars;
const auto addModeGate = [&](AstNodeExpr* modeArrayp, uint32_t index) {
AstCMethodHard* const atp = new AstCMethodHard{fl, modeArrayp, VCMethod::ARRAY_AT,
new AstConst{fl, index}};
atp->dtypeSetUInt32();
if (gatep) {
gatep = new AstLogAnd{fl, gatep, atp};
gatep->dtypeSetBit();
} else {
gatep = atp;
}
};
const auto staticModeRead = [&](AstVar* varp) -> AstNodeExpr* {
AstClass* const ownerp = VN_CAST(varp->user2p(), Class);
AstVar* const smodep = ownerp ? getStaticRandModeVar(ownerp) : nullptr;
if (!smodep) return nullptr;
return new AstVarRef{fl, VN_AS(smodep->user2p(), NodeModule), smodep,
VAccess::READ};
};
distp->exprp()->foreach([&](const AstNodeVarRef* refp) {
AstVar* const varp = refp->varp();
const RandomizeMode rmode = {.asInt = varp->user1()};
if (!rmode.usesMode || !gatedVars.insert(varp).second) return;
if (varp->lifetime().isStatic()) {
if (AstNodeExpr* const readp = staticModeRead(varp))
addModeGate(readp, rmode.index);
} else if (randModeVarp) {
addModeGate(new AstVarRef{fl, VN_AS(randModeVarp->user2p(), NodeModule),
randModeVarp, VAccess::READ},
rmode.index);
}
});
distp->exprp()->foreach([&](const AstMemberSel* mselp) {
AstVar* const varp = mselp->varp();
const RandomizeMode rmode = {.asInt = varp->user1()};
if (!rmode.usesMode || !gatedVars.insert(varp).second) return;
if (varp->lifetime().isStatic()) {
if (AstNodeExpr* const readp = staticModeRead(varp))
addModeGate(readp, rmode.index);
return;
}
AstVar* const memberModep
= getRandModeVarFromClass(VN_AS(varp->user2p(), NodeModule));
if (!memberModep) return;
AstMemberSel* const modeSelp
= new AstMemberSel{fl, mselp->fromp()->cloneTreePure(false), memberModep};
modeSelp->dtypep(memberModep->dtypep());
addModeGate(modeSelp, rmode.index);
});
if (chainp && gatep) {
AstNodeExpr* unionExprp = nullptr;
for (auto& bucket : buckets) {
AstNodeExpr* termp;
if (const AstInsideRange* const irp = VN_CAST(bucket.rangep, InsideRange)) {
AstNodeExpr* const exprCopy1p = distp->exprp()->cloneTreePure(false);
exprCopy1p->user1(true);
AstNodeExpr* const exprCopy2p = distp->exprp()->cloneTreePure(false);
exprCopy2p->user1(true);
AstGte* const gtep
= new AstGte{fl, exprCopy1p, irp->lhsp()->cloneTreePure(false)};
gtep->user1(true);
AstLte* const ltep
= new AstLte{fl, exprCopy2p, irp->rhsp()->cloneTreePure(false)};
ltep->user1(true);
termp = new AstLogAnd{fl, gtep, ltep};
termp->user1(true);
} else {
AstNodeExpr* const exprCopyp = distp->exprp()->cloneTreePure(false);
exprCopyp->user1(true);
termp = new AstEq{fl, exprCopyp, bucket.rangep->cloneTreePure(false)};
termp->user1(true);
}
// Compile-time zero weights never enter buckets; a weight that
// is zero only at runtime excludes the bucket's values too
bool runtimeWeight = false;
bucket.weightExprp->foreach(
[&](const AstNodeVarRef*) { runtimeWeight = 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;
}
}
chainp = new AstConstraintIf{fl, gatep, chainp,
new AstConstraintExpr{fl, unionExprp}};
}
if (chainp) {
constrExprp->replaceWith(chainp);
VL_DO_DANGLING(pushDeletep(constrExprp), constrExprp);
@ -4597,7 +4710,8 @@ 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,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,281 @@
// 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 dist constraint on a variable that is frozen at randomize() time
// (rand_mode(0)) degrades to a membership test: weights cannot cause the
// solver to fail (IEEE 1800-2023 18.5.3), but a frozen value outside the set
// still violates membership.
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 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 RtWeight;
rand uint x;
uint w;
constraint c {x dist {5 := 9, 70 := w};}
endclass
module t;
int ok;
initial begin
SingleVals sv;
RangeVals rv;
MixVals mv;
StaticVar st;
Holder h;
MultiVar m;
RtWeight r;
// 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);
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
// 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, never fails on a valid set).
sv = new;
for (int i = 0; i < 8; ++i) begin
ok = sv.randomize();
`checkd(ok, 1);
if (sv.x != 5 && sv.x != 1000000) `checkd(0, 1);
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
// 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
// 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) `checkd(0, 1);
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
// 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
$write("*-* All Finished *-*\n");
$finish;
end
endmodule