Support constraint imperfect distributions (#6811) (#7168)

This commit is contained in:
Yilou Wang 2026-03-03 17:23:14 +01:00 committed by GitHub
parent 5f3d475736
commit 3bc73cc768
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 405 additions and 13 deletions

View File

@ -746,6 +746,14 @@ void VlRandomizer::soft(std::string&& constraint, const char* /*filename*/, uint
m_softConstraints.emplace_back(std::move(constraint));
}
void VlRandomizer::disable_soft(const std::string& varName) {
// IEEE 1800-2017 18.5.13: Remove all soft constraints referencing the variable
m_softConstraints.erase(
std::remove_if(m_softConstraints.begin(), m_softConstraints.end(),
[&](const std::string& c) { return c.find(varName) != std::string::npos; }),
m_softConstraints.end());
}
void VlRandomizer::clearConstraints() {
m_constraints.clear();
m_constraints_line.clear();

View File

@ -596,6 +596,7 @@ public:
const char* source = "");
void soft(std::string&& constraint, const char* filename = "", uint32_t linenum = 0,
const char* source = "");
void disable_soft(const std::string& varName);
void clearConstraints();
void clearAll(); // Clear both constraints and variables
void markRandc(const char* name); // Mark variable as randc for cyclic tracking

View File

@ -814,6 +814,7 @@ public:
RANDOMIZER_BASIC_STD_RANDOMIZATION,
RANDOMIZER_CLEARCONSTRAINTS,
RANDOMIZER_CLEARALL,
RANDOMIZER_DISABLE_SOFT,
RANDOMIZER_HARD,
RANDOMIZER_SOFT,
RANDOMIZER_UNIQUE,
@ -951,6 +952,7 @@ inline std::ostream& operator<<(std::ostream& os, const VCMethod& rhs) {
{RANDOMIZER_BASIC_STD_RANDOMIZATION, "basicStdRandomization", false}, \
{RANDOMIZER_CLEARCONSTRAINTS, "clearConstraints", false}, \
{RANDOMIZER_CLEARALL, "clearAll", false}, \
{RANDOMIZER_DISABLE_SOFT, "disable_soft", false}, \
{RANDOMIZER_HARD, "hard", false}, \
{RANDOMIZER_SOFT, "soft", false}, \
{RANDOMIZER_UNIQUE, "rand_unique", false}, \

View File

@ -1268,7 +1268,7 @@ class AstDist final : public AstNodeExpr {
// @astgen op2 := itemsp : List[AstDistItem]
public:
AstDist(FileLine* fl, AstNodeExpr* exprp, AstDistItem* itemsp)
: ASTGEN_SUPER_Inside(fl) {
: ASTGEN_SUPER_Dist(fl) {
this->exprp(exprp);
addItemsp(itemsp);
dtypeSetBit();

View File

@ -1785,6 +1785,7 @@ class ConstraintExprVisitor final : public VNVisitor {
newp = new AstLogIf{fl, new AstNot{fl, nodep->condp()->unlinkFrBack()}, elsep};
}
if (newp) {
newp->dtypeSetBit(); // Result is boolean (prevents bare-var != 0 wrapping)
newp->user1(true); // Assume result-dependent
nodep->replaceWith(new AstConstraintExpr{fl, newp});
} else {
@ -1963,6 +1964,45 @@ class ConstraintExprVisitor final : public VNVisitor {
}
void visit(AstConstraintExpr* nodep) override {
// IEEE 1800-2017 18.5.13: "disable soft" removes all soft constraints
// referencing the specified variable. Pass the variable name directly
// instead of going through SMT lowering.
if (nodep->isDisableSoft()) {
// Extract variable name from expression (VarRef or MemberSel)
std::string varName;
if (const AstNodeVarRef* const vrefp = VN_CAST(nodep->exprp(), NodeVarRef)) {
varName = vrefp->name();
} else if (const AstMemberSel* const mselp = VN_CAST(nodep->exprp(), MemberSel)) {
varName = mselp->name();
} else {
nodep->v3fatalSrc("Unexpected expression type in disable soft");
return;
}
AstCMethodHard* const callp = new AstCMethodHard{
nodep->fileline(),
new AstVarRef{nodep->fileline(), VN_AS(m_genp->user2p(), NodeModule), m_genp,
VAccess::READWRITE},
VCMethod::RANDOMIZER_DISABLE_SOFT,
new AstConst{nodep->fileline(), AstConst::String{}, varName}};
callp->dtypeSetVoid();
nodep->replaceWith(callp->makeStmt());
VL_DO_DANGLING(nodep->deleteTree(), nodep);
return;
}
// IEEE 1800-2017 18.5.1: A bare expression used as a constraint is
// implicitly treated as "expr != 0" when wider than 1 bit.
// Must wrap before iterateChildren, which converts to SMT format.
{
AstNodeExpr* const exprp = nodep->exprp();
if (exprp->width() > 1) {
FileLine* const fl = exprp->fileline();
V3Number numZero{fl, exprp->width(), 0};
AstNodeExpr* const neqp
= new AstNeq{fl, exprp->unlinkFrBack(), new AstConst{fl, numZero}};
neqp->user1(true); // Mark as rand-dependent for SMT path
nodep->exprp(neqp);
}
}
iterateChildren(nodep);
if (m_wantSingle) {
nodep->replaceWith(nodep->exprp()->unlinkFrBack());
@ -2506,6 +2546,7 @@ class RandomizeVisitor final : public VNVisitor {
AstDynArrayDType* m_dynarrayDtp = nullptr; // Dynamic array type (for rand mode)
size_t m_enumValueTabCount = 0; // Number of tables with enum values created
int m_randCaseNum = 0; // Randcase number within a module for var naming
int m_distNum = 0; // Dist bucket variable counter within a module for var naming
std::map<std::string, AstCDType*> m_randcDtypes; // RandC data type deduplication
AstConstraint* m_constraintp = nullptr; // Current constraint
std::set<std::string> m_writtenVars; // Track write_var calls per class to avoid duplicates
@ -3551,6 +3592,178 @@ 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) {
for (AstNode *nextip, *itemp = constrItemsp; itemp; itemp = nextip) {
nextip = itemp->nextp();
AstConstraintExpr* const constrExprp = VN_CAST(itemp, ConstraintExpr);
if (!constrExprp) continue;
AstDist* const distp = VN_CAST(constrExprp->exprp(), Dist);
if (!distp) continue;
FileLine* const fl = distp->fileline();
struct BucketInfo final {
AstNodeExpr* rangep;
AstNodeExpr* weightExprp; // Effective weight as AST expression
};
std::vector<BucketInfo> buckets;
for (AstDistItem* ditemp = distp->itemsp(); ditemp;
ditemp = VN_AS(ditemp->nextp(), DistItem)) {
// Skip compile-time zero weights
if (const AstConst* const constp = VN_CAST(ditemp->weightp(), Const)) {
if (constp->toUQuad() == 0) continue;
}
// Clone and extend weight to 64-bit
AstNodeExpr* weightExprp
= new AstExtend{fl, ditemp->weightp()->cloneTreePure(false), 64};
// := is per-value weight; for ranges multiply by range size
if (!ditemp->isWhole()) {
if (const AstInsideRange* const irp = VN_CAST(ditemp->rangep(), InsideRange)) {
const AstConst* const lop = VN_CAST(irp->lhsp(), Const);
const AstConst* const hip = VN_CAST(irp->rhsp(), Const);
AstNodeExpr* rangeSizep;
if (lop && hip) {
const uint64_t rangeSize = hip->toUQuad() - lop->toUQuad() + 1;
rangeSizep = new AstConst{fl, AstConst::Unsized64{}, rangeSize};
} else {
// Variable range bounds: (hi - lo + 1) at runtime
rangeSizep = new AstAdd{
fl, new AstConst{fl, AstConst::Unsized64{}, 1},
new AstSub{
fl, new AstExtend{fl, irp->rhsp()->cloneTreePure(false), 64},
new AstExtend{fl, irp->lhsp()->cloneTreePure(false), 64}}};
rangeSizep->dtypeSetUInt64();
}
weightExprp = new AstMul{fl, weightExprp, rangeSizep};
weightExprp->dtypeSetUInt64();
}
}
buckets.push_back({ditemp->rangep(), weightExprp});
}
if (buckets.empty()) {
// All weights are zero: dist is vacuously true (unconstrained)
AstConstraintExpr* const truep
= new AstConstraintExpr{fl, new AstConst{fl, AstConst::BitTrue{}}};
constrExprp->replaceWith(truep);
VL_DO_DANGLING(pushDeletep(constrExprp), constrExprp);
continue;
}
// Build totalWeight expression: w[0] + w[1] + ... + w[N-1]
AstNodeExpr* totalWeightExprp = nullptr;
for (auto& bucket : buckets) {
if (!totalWeightExprp) {
totalWeightExprp = bucket.weightExprp->cloneTreePure(false);
} else {
totalWeightExprp = new AstAdd{fl, totalWeightExprp,
bucket.weightExprp->cloneTreePure(false)};
totalWeightExprp->dtypeSetUInt64();
}
}
// Store totalWeight in temp var (evaluated once, used twice)
const int distId = m_distNum++;
const std::string totalName = "__Vdist_total" + cvtToStr(distId);
AstVar* const totalVarp
= new AstVar{fl, VVarType::BLOCKTEMP, totalName, taskp->findUInt64DType()};
totalVarp->noSubst(true);
totalVarp->lifetime(VLifetime::AUTOMATIC_EXPLICIT);
totalVarp->funcLocal(true);
totalVarp->isInternal(true);
taskp->addStmtsp(totalVarp);
taskp->addStmtsp(
new AstAssign{fl, new AstVarRef{fl, totalVarp, VAccess::WRITE}, totalWeightExprp});
// bucketVar = (rand64() % totalWeight) + 1
const std::string bucketName = "__Vdist_bucket" + cvtToStr(distId);
AstVar* const bucketVarp
= new AstVar{fl, VVarType::BLOCKTEMP, bucketName, taskp->findUInt64DType()};
bucketVarp->noSubst(true);
bucketVarp->lifetime(VLifetime::AUTOMATIC_EXPLICIT);
bucketVarp->funcLocal(true);
bucketVarp->isInternal(true);
taskp->addStmtsp(bucketVarp);
AstNodeExpr* randp = new AstRand{fl, nullptr, false};
randp->dtypeSetUInt64();
taskp->addStmtsp(new AstAssign{
fl, new AstVarRef{fl, bucketVarp, VAccess::WRITE},
new AstAdd{
fl, new AstConst{fl, AstConst::Unsized64{}, 1},
new AstModDiv{fl, randp, new AstVarRef{fl, totalVarp, VAccess::READ}}}});
// Build cumulative sum expressions forward: cumSum[i] = w[0]+...+w[i]
std::vector<AstNodeExpr*> cumSums;
AstNodeExpr* runningSump = nullptr;
for (size_t i = 0; i < buckets.size(); ++i) {
if (!runningSump) {
runningSump = buckets[i].weightExprp->cloneTreePure(false);
} else {
runningSump = new AstAdd{fl, runningSump,
buckets[i].weightExprp->cloneTreePure(false)};
runningSump->dtypeSetUInt64();
}
cumSums.push_back(runningSump->cloneTreePure(true));
}
// Build ConstraintIf chain backward (last bucket is unconditional default)
AstNode* chainp = nullptr;
for (int i = static_cast<int>(buckets.size()) - 1; i >= 0; --i) {
AstNodeExpr* constraintExprp;
if (const AstInsideRange* const irp = VN_CAST(buckets[i].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);
constraintExprp = new AstLogAnd{fl, gtep, ltep};
constraintExprp->user1(true);
} else {
AstNodeExpr* const exprCopyp = distp->exprp()->cloneTreePure(false);
exprCopyp->user1(true);
constraintExprp
= new AstEq{fl, exprCopyp, buckets[i].rangep->cloneTreePure(false)};
constraintExprp->user1(true);
}
AstConstraintExpr* const thenp = new AstConstraintExpr{fl, constraintExprp};
if (!chainp) {
chainp = thenp;
} else {
AstNodeExpr* const condp
= new AstLte{fl, new AstVarRef{fl, bucketVarp, VAccess::READ}, cumSums[i]};
chainp = new AstConstraintIf{fl, condp, thenp, chainp};
}
}
if (chainp) {
constrExprp->replaceWith(chainp);
VL_DO_DANGLING(pushDeletep(constrExprp), constrExprp);
}
// Clean up nodes used only as clone templates (never inserted into tree)
for (auto& bucket : buckets) {
VL_DO_DANGLING(pushDeletep(bucket.weightExprp), bucket.weightExprp);
}
VL_DO_DANGLING(pushDeletep(runningSump), runningSump);
// Last cumSum is unused (last bucket is unconditional default)
pushDeletep(cumSums.back());
}
}
// VISITORS
void visit(AstNodeModule* nodep) override {
VL_RESTORER(m_modp);
@ -3567,8 +3780,10 @@ class RandomizeVisitor final : public VNVisitor {
void visit(AstClass* nodep) override {
VL_RESTORER(m_modp);
VL_RESTORER(m_randCaseNum);
VL_RESTORER(m_distNum);
m_modp = nodep;
m_randCaseNum = 0;
m_distNum = 0;
m_writtenVars.clear(); // Each class has its own set of written variables
iterateChildren(nodep);
@ -3616,6 +3831,7 @@ class RandomizeVisitor final : public VNVisitor {
}
if (constrp->itemsp()) expandUniqueElementList(constrp->itemsp());
if (constrp->itemsp()) lowerDistConstraints(taskp, constrp->itemsp());
ConstraintExprVisitor{classp, m_memberMap, constrp->itemsp(), nullptr,
genp, randModeVarp, m_writtenVars};
if (constrp->itemsp()) {

View File

@ -3126,7 +3126,7 @@ class WidthVisitor final : public VNVisitor {
}
void visit(AstDist* nodep) override {
// x dist {a :/ p, b :/ q} --> (p > 0 && x == a) || (q > 0 && x == b)
nodep->v3warn(CONSTRAINTIGN, "Constraint expression ignored (imperfect distribution)");
// (only outside constraints; inside constraints V3Randomize handles weighted selection)
userIterateAndNext(nodep->exprp(), WidthVP{CONTEXT_DET, PRELIM}.p());
for (AstNode *nextip, *itemp = nodep->itemsp(); itemp; itemp = nextip) {
nextip = itemp->nextp(); // iterate may cause the node to get replaced
@ -3163,6 +3163,23 @@ class WidthVisitor final : public VNVisitor {
if (!VN_IS(itemp, InsideRange))
iterateCheck(nodep, "Dist Item", itemp, CONTEXT_DET, FINAL, subDTypep, EXTEND_EXP);
}
// Inside a constraint, V3Randomize handles dist lowering with proper weights,
// but only for simple scalar/range items. Container-type items (queues, arrays)
// must be lowered here via insideItem() which knows how to expand them.
if (m_constraintp) {
bool canLower = true;
for (AstDistItem* ditemp = nodep->itemsp(); ditemp;
ditemp = VN_AS(ditemp->nextp(), DistItem)) {
if (!VN_IS(ditemp->rangep(), Const) && !VN_IS(ditemp->rangep(), InsideRange)) {
canLower = false;
break;
}
}
if (canLower) return;
}
// Outside constraint: lower to inside (ignores weights)
AstNodeExpr* newp = nullptr;
for (AstDistItem* itemp = nodep->itemsp(); itemp;
itemp = VN_AS(itemp->nextp(), DistItem)) {

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,129 @@
// 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);
`define check_range(gotv,minv,maxv) do if ((gotv) < (minv) || (gotv) > (maxv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d-%0d\n", `__FILE__,`__LINE__, (gotv), (minv), (maxv)); `stop; end while(0);
// verilog_format: on
class DistScalar;
rand bit [7:0] x;
constraint c { x dist { 8'd0 := 1, 8'd255 := 3 }; }
endclass
class DistRange;
rand bit [7:0] x;
constraint c { x dist { [8'd0:8'd9] :/ 1, [8'd10:8'd19] :/ 3 }; }
endclass
class DistZeroWeight;
rand bit [7:0] x;
constraint c { x dist { 8'd0 := 0, 8'd1 := 1, 8'd2 := 1 }; }
endclass
class DistAllZeroWeight;
rand bit [7:0] x;
constraint c { x dist { 8'd0 := 0, 8'd1 := 0, 8'd2 := 0 }; }
endclass
class DistVarWeight;
rand bit [7:0] x;
int w1, w2;
constraint c { x dist { 8'd0 := w1, 8'd255 := w2 }; }
endclass
class DistVarWeightRange;
rand bit [7:0] x;
int w1, w2;
constraint c { x dist { [8'd0:8'd9] :/ w1, [8'd10:8'd19] :/ w2 }; }
endclass
module t;
initial begin
DistScalar sc;
DistRange rg;
DistZeroWeight zw;
DistAllZeroWeight azw;
DistVarWeight vw;
DistVarWeightRange vwr;
int count_high;
int count_range_high;
int total;
total = 2000;
// := scalar weights: expect ~75% for value 255
sc = new;
count_high = 0;
repeat (total) begin
`checkd(sc.randomize(), 1);
if (sc.x == 8'd255) count_high++;
else `checkd(sc.x, 0);
end
`check_range(count_high, total * 60 / 100, total * 90 / 100);
// :/ range weights: expect ~75% in [10:19]
rg = new;
count_range_high = 0;
repeat (total) begin
`checkd(rg.randomize(), 1);
if (rg.x >= 8'd10 && rg.x <= 8'd19) count_range_high++;
else if (rg.x > 8'd9) begin
$write("%%Error: x=%0d outside valid range [0:19]\n", rg.x);
`stop;
end
end
`check_range(count_range_high, total * 60 / 100, total * 90 / 100);
// Zero weight: value 0 must never appear
zw = new;
repeat (total) begin
`checkd(zw.randomize(), 1);
if (zw.x == 8'd0) begin
$write("%%Error: zero-weight value 0 was selected\n");
`stop;
end
`check_range(zw.x, 1, 2);
end
// All-zero weights: dist constraint is effectively unconstrained, randomize succeeds
azw = new;
repeat (20) begin
`checkd(azw.randomize(), 1);
end
// Variable := scalar weights: w1=1, w2=3 => expect ~75% for value 255
vw = new;
vw.w1 = 1;
vw.w2 = 3;
count_high = 0;
repeat (total) begin
`checkd(vw.randomize(), 1);
if (vw.x == 8'd255) count_high++;
else `checkd(vw.x, 0);
end
`check_range(count_high, total * 60 / 100, total * 90 / 100);
// Variable :/ range weights: w1=1, w2=3 => expect ~75% in [10:19]
vwr = new;
vwr.w1 = 1;
vwr.w2 = 3;
count_range_high = 0;
repeat (total) begin
`checkd(vwr.randomize(), 1);
if (vwr.x >= 8'd10 && vwr.x <= 8'd19) count_range_high++;
else if (vwr.x > 8'd9) begin
$write("%%Error: x=%0d outside valid range [0:19]\n", vwr.x);
`stop;
end
end
`check_range(count_range_high, total * 60 / 100, total * 90 / 100);
$write("*-* All Finished *-*\n");
$finish;
end
endmodule

View File

@ -1,7 +0,0 @@
%Warning-CONSTRAINTIGN: t/t_randomize.v:22:14: Constraint expression ignored (imperfect distribution)
: ... note: In instance 't'
22 | length dist { [0:1], [2:5] :/ 2, 6 := 6, 7 := 10, 1};
| ^~~~
... For warning description see https://verilator.org/warn/CONSTRAINTIGN?v=latest
... Use "/* verilator lint_off CONSTRAINTIGN */" and lint_on around source to disable this message.
%Error: Exiting due to

View File

@ -9,8 +9,13 @@
import vltest_bootstrap
test.scenarios('vlt')
test.scenarios('simulator')
test.lint(fails=True, expect_filename=test.golden_filename)
if not test.have_solver:
test.skip("No constraint solver installed")
test.compile()
test.execute()
test.passes()

View File

@ -11,7 +11,7 @@ class Packet;
rand bit if_4;
rand bit iff_5_6;
/*rand*/ int array[2]; // 2,4,6 // TODO: add rand when supported
rand int array[2]; // 2,4,6
constraint empty {}
@ -58,7 +58,7 @@ module t;
automatic int v;
automatic bit if_4 = '0;
// TODO not testing constrained values
p = new;
v = p.randomize();
if (v != 1) $stop;
v = p.randomize() with {};