Fix dist constraint on a frozen variable failing randomization
This commit is contained in:
parent
d84af81a11
commit
06907c5358
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue