diff --git a/include/verilated_random.cpp b/include/verilated_random.cpp index 4ed746e86..f800b2ea8 100644 --- a/include/verilated_random.cpp +++ b/include/verilated_random.cpp @@ -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(); diff --git a/include/verilated_random.h b/include/verilated_random.h index 993778b55..010a52a9f 100644 --- a/include/verilated_random.h +++ b/include/verilated_random.h @@ -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 diff --git a/src/V3AstAttr.h b/src/V3AstAttr.h index bfe9ba1fb..bf5ba0ae6 100644 --- a/src/V3AstAttr.h +++ b/src/V3AstAttr.h @@ -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}, \ diff --git a/src/V3AstNodeExpr.h b/src/V3AstNodeExpr.h index 34b0bf105..664d7946b 100644 --- a/src/V3AstNodeExpr.h +++ b/src/V3AstNodeExpr.h @@ -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(); diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index 57c257796..5300a31ac 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -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 m_randcDtypes; // RandC data type deduplication AstConstraint* m_constraintp = nullptr; // Current constraint std::set 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 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 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(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()) { diff --git a/src/V3Width.cpp b/src/V3Width.cpp index c96dcc561..06415443a 100644 --- a/src/V3Width.cpp +++ b/src/V3Width.cpp @@ -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)) { diff --git a/test_regress/t/t_constraint_dist_weight.py b/test_regress/t/t_constraint_dist_weight.py new file mode 100755 index 000000000..db1adb3f9 --- /dev/null +++ b/test_regress/t/t_constraint_dist_weight.py @@ -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() diff --git a/test_regress/t/t_constraint_dist_weight.v b/test_regress/t/t_constraint_dist_weight.v new file mode 100644 index 000000000..8312d6650 --- /dev/null +++ b/test_regress/t/t_constraint_dist_weight.v @@ -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 diff --git a/test_regress/t/t_randomize.out b/test_regress/t/t_randomize.out deleted file mode 100644 index 1e9892a78..000000000 --- a/test_regress/t/t_randomize.out +++ /dev/null @@ -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 diff --git a/test_regress/t/t_randomize.py b/test_regress/t/t_randomize.py index 3160d0589..ab048b5e8 100755 --- a/test_regress/t/t_randomize.py +++ b/test_regress/t/t_randomize.py @@ -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() diff --git a/test_regress/t/t_randomize.v b/test_regress/t/t_randomize.v index 2c613ac94..0a0231de6 100644 --- a/test_regress/t/t_randomize.v +++ b/test_regress/t/t_randomize.v @@ -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 {};