From 06907c5358ba98624aa935413f063794f424b1bd Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Sat, 4 Jul 2026 10:48:22 +0200 Subject: [PATCH 1/8] Fix dist constraint on a frozen variable failing randomization --- src/V3Randomize.cpp | 128 +++++++- .../t/t_constraint_dist_randmode_frozen.py | 21 ++ .../t/t_constraint_dist_randmode_frozen.v | 281 ++++++++++++++++++ 3 files changed, 423 insertions(+), 7 deletions(-) create mode 100755 test_regress/t/t_constraint_dist_randmode_frozen.py create mode 100644 test_regress/t/t_constraint_dist_randmode_frozen.v diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index 3fcaf5535..3d8bf02b1 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -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 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& sizeArrays = m_sizeConstrainedArrays[classp]; ConstraintExprVisitor{classp, m_memberMap, constrp->itemsp(), nullptr, genp, randModeVarp, diff --git a/test_regress/t/t_constraint_dist_randmode_frozen.py b/test_regress/t/t_constraint_dist_randmode_frozen.py new file mode 100755 index 000000000..db1adb3f9 --- /dev/null +++ b/test_regress/t/t_constraint_dist_randmode_frozen.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_randmode_frozen.v b/test_regress/t/t_constraint_dist_randmode_frozen.v new file mode 100644 index 000000000..7b7dcc46b --- /dev/null +++ b/test_regress/t/t_constraint_dist_randmode_frozen.v @@ -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 From 3c7f8b5dfcf7b887dd03f8b0c866008171b96a6b Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Wed, 8 Jul 2026 22:43:02 +0200 Subject: [PATCH 2/8] Refactor dist-randmode lowering into helpers and cover if/foreach paths --- src/V3Randomize.cpp | 390 +++++++++--------- .../t/t_constraint_dist_randmode_frozen.v | 42 +- 2 files changed, 228 insertions(+), 204 deletions(-) diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index ea5b64bd5..9585d1b01 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -3314,9 +3314,7 @@ class RandomizeVisitor final : public VNVisitor { m_dynarrayDtp->dtypep(m_dynarrayDtp); v3Global.rootp()->typeTablep()->addTypesp(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) + // 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); @@ -4459,6 +4457,188 @@ class RandomizeVisitor final : public VNVisitor { return new AstConstraintIf{fl, condp, thenBodyp, nullptr}; } + struct DistBucket final { + AstNodeExpr* rangep; // A single value or an InsideRange + AstNodeExpr* weightExprp; // Effective 64-bit weight (range weight scaled by size) + }; + + // Non-zero-weight buckets with each weight extended to 64 bits. + std::vector collectDistBuckets(AstDist* distp) { + FileLine* const fl = distp->fileline(); + std::vector buckets; + for (AstDistItem* ditemp = distp->itemsp(); ditemp; + ditemp = VN_AS(ditemp->nextp(), DistItem)) { + if (const AstConst* const constp = VN_CAST(ditemp->weightp(), Const)) { + if (constp->toUQuad() == 0) continue; + } + AstNodeExpr* weightExprp + = new AstExtend{fl, ditemp->weightp()->cloneTreePure(false), 64}; + // := on a range weights every element, so scale by the 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 { + 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}); + } + return buckets; + } + + // Hard constraint that the dist value stays inside the union of its ranges + // (IEEE 1800-2023 18.5.3: values outside the set must never appear). + AstConstraintExpr* buildDistMembership(AstDist* distp, + const std::vector& buckets) { + FileLine* const fl = distp->fileline(); + const bool isSigned = distp->exprp()->isSigned(); + AstNodeExpr* unionExprp = nullptr; + for (const auto& bucket : buckets) { + AstNodeExpr* memberp; + if (const AstInsideRange* const irp = VN_CAST(bucket.rangep, InsideRange)) { + AstNodeExpr* const gtExprp = distp->exprp()->cloneTreePure(false); + AstNodeExpr* const ltExprp = distp->exprp()->cloneTreePure(false); + gtExprp->user1(true); + ltExprp->user1(true); + AstNodeExpr* const gep + = isSigned ? static_cast( + new AstGteS{fl, gtExprp, irp->lhsp()->cloneTreePure(false)}) + : static_cast( + new AstGte{fl, gtExprp, irp->lhsp()->cloneTreePure(false)}); + AstNodeExpr* const lep + = isSigned ? static_cast( + new AstLteS{fl, ltExprp, irp->rhsp()->cloneTreePure(false)}) + : static_cast( + new AstLte{fl, ltExprp, irp->rhsp()->cloneTreePure(false)}); + gep->user1(true); + lep->user1(true); + memberp = new AstLogAnd{fl, gep, lep}; + } else { + AstNodeExpr* const eqExprp = distp->exprp()->cloneTreePure(false); + eqExprp->user1(true); + memberp = new AstEq{fl, eqExprp, bucket.rangep->cloneTreePure(false)}; + } + memberp->user1(true); + if (!unionExprp) { + unionExprp = memberp; + } else { + unionExprp = new AstLogOr{fl, memberp, unionExprp}; + unionExprp->user1(true); + } + } + return new AstConstraintExpr{fl, unionExprp}; + } + + // Gate the weighted chain on the rand_mode bits of the dist's variables, so a + // frozen variable requires only membership, not a freshly drawn bucket. + AstNode* gateFrozenDist(AstNode* chainp, AstDist* distp, + const std::vector& buckets, AstVar* randModeVarp) { + if (!chainp) return chainp; + FileLine* const fl = distp->fileline(); + AstNodeExpr* gatep = nullptr; + std::unordered_set 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}; + }; + // Direct var references and sub-object member references each carry a mode bit. + 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 (!gatep) return chainp; + + AstNodeExpr* unionExprp = nullptr; + for (auto& bucket : buckets) { + AstNodeExpr* termp; + if (const AstInsideRange* const irp = VN_CAST(bucket.rangep, InsideRange)) { + AstNodeExpr* const gtExprp = distp->exprp()->cloneTreePure(false); + AstNodeExpr* const ltExprp = distp->exprp()->cloneTreePure(false); + gtExprp->user1(true); + ltExprp->user1(true); + AstGte* const gtep = new AstGte{fl, gtExprp, irp->lhsp()->cloneTreePure(false)}; + gtep->user1(true); + AstLte* const ltep = new AstLte{fl, ltExprp, irp->rhsp()->cloneTreePure(false)}; + ltep->user1(true); + termp = new AstLogAnd{fl, gtep, ltep}; + termp->user1(true); + } else { + AstNodeExpr* const eqExprp = distp->exprp()->cloneTreePure(false); + eqExprp->user1(true); + termp = new AstEq{fl, eqExprp, bucket.rangep->cloneTreePure(false)}; + termp->user1(true); + } + // 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; + } + } + return new AstConstraintIf{fl, gatep, chainp, new AstConstraintExpr{fl, unionExprp}}; + } + // Replace AstDist with weighted bucket selection via AstConstraintIf chain. // Supports both constant and variable weight expressions. void lowerDistConstraints(AstTask* taskp, AstNode* constrItemsp, AstVar* randModeVarp, @@ -4484,21 +4664,16 @@ class RandomizeVisitor final : public VNVisitor { for (AstNode *nextip, *itemp = constrItemsp; itemp; itemp = nextip) { nextip = itemp->nextp(); - // Recursively handle ConstraintIf nodes (dist can be inside if/else) + // dist can appear inside an if/else or foreach constraint. if (AstConstraintIf* const cifp = VN_CAST(itemp, ConstraintIf)) { - if (cifp->thensp()) // LCOV_EXCL_LINE - lowerDistConstraints(taskp, cifp->thensp(), randModeVarp, // LCOV_EXCL_LINE - foreachp); // LCOV_EXCL_LINE + if (cifp->thensp()) + lowerDistConstraints(taskp, cifp->thensp(), randModeVarp, foreachp); if (cifp->elsesp()) lowerDistConstraints(taskp, cifp->elsesp(), randModeVarp, foreachp); continue; } - - // Recursively handle ConstraintForeach nodes (dist can be inside foreach) if (AstConstraintForeach* const cfep = VN_CAST(itemp, ConstraintForeach)) { - if (cfep->bodyp()) // LCOV_EXCL_LINE - lowerDistConstraints(taskp, cfep->bodyp(), randModeVarp, // LCOV_EXCL_LINE - cfep); // LCOV_EXCL_LINE + if (cfep->bodyp()) lowerDistConstraints(taskp, cfep->bodyp(), randModeVarp, cfep); continue; } @@ -4528,48 +4703,7 @@ class RandomizeVisitor final : public VNVisitor { 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}); - } + std::vector buckets = collectDistBuckets(distp); if (buckets.empty()) { // All weights are zero: dist is vacuously true (unconstrained) @@ -4580,46 +4714,7 @@ class RandomizeVisitor final : public VNVisitor { continue; } - // IEEE 1800-2023 18.5.3: values not in the distribution must never appear. - // Build the union of all non-zero-weight ranges as a single hard ConstraintExpr - AstNodeExpr* unionExprp = nullptr; - for (const auto& bucket : buckets) { - AstNodeExpr* memberp; - if (const AstInsideRange* const irp = VN_CAST(bucket.rangep, InsideRange)) { - // (distExpr >= lo) && (distExpr <= hi); signed comparisons for signed vars - const bool isSigned = distp->exprp()->isSigned(); - AstNodeExpr* const distExprGtep = distp->exprp()->cloneTreePure(false); - AstNodeExpr* const distExprLtep = distp->exprp()->cloneTreePure(false); - distExprGtep->user1(true); - distExprLtep->user1(true); - AstNodeExpr* const gep - = isSigned ? static_cast(new AstGteS{ - fl, distExprGtep, irp->lhsp()->cloneTreePure(false)}) - : static_cast(new AstGte{ - fl, distExprGtep, irp->lhsp()->cloneTreePure(false)}); - AstNodeExpr* const lep - = isSigned ? static_cast(new AstLteS{ - fl, distExprLtep, irp->rhsp()->cloneTreePure(false)}) - : static_cast(new AstLte{ - fl, distExprLtep, irp->rhsp()->cloneTreePure(false)}); - gep->user1(true); - lep->user1(true); - memberp = new AstLogAnd{fl, gep, lep}; - } else { - // distExpr == val - AstNodeExpr* const distExprCopyp = distp->exprp()->cloneTreePure(false); - distExprCopyp->user1(true); - memberp = new AstEq{fl, distExprCopyp, bucket.rangep->cloneTreePure(false)}; - } - memberp->user1(true); - if (!unionExprp) { - unionExprp = memberp; - } else { - unionExprp = new AstLogOr{fl, memberp, unionExprp}; - unionExprp->user1(true); - } - } - AstConstraintExpr* const membershipp = new AstConstraintExpr{fl, unionExprp}; + AstConstraintExpr* const membershipp = buildDistMembership(distp, buckets); // Build totalWeight expression: w[0] + w[1] + ... + w[N-1] AstNodeExpr* totalWeightExprp = nullptr; @@ -4746,112 +4841,7 @@ 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 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}}; - } + chainp = gateFrozenDist(chainp, distp, buckets, randModeVarp); if (chainp) { constrExprp->replaceWith(chainp); diff --git a/test_regress/t/t_constraint_dist_randmode_frozen.v b/test_regress/t/t_constraint_dist_randmode_frozen.v index 7b7dcc46b..461296871 100644 --- a/test_regress/t/t_constraint_dist_randmode_frozen.v +++ b/test_regress/t/t_constraint_dist_randmode_frozen.v @@ -9,10 +9,7 @@ `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. +// A frozen dist variable degrades to a membership test (IEEE 1800-2023 18.5.3). typedef int unsigned uint; class Base; @@ -36,6 +33,20 @@ 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; @@ -87,6 +98,8 @@ module t; Holder h; MultiVar m; RtWeight r; + DistInIf di; + DistInForeach df; // Frozen in the low-weight bucket -> succeeds, value untouched. sv = new; @@ -275,6 +288,27 @@ module t; `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) `checkd(0, 1); + end else begin + `checkd(di.x, 4242); + end + 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) `checkd(0, 1); + end + $write("*-* All Finished *-*\n"); $finish; end From c99e17065a4fb3a957cde3d38835e2c20c00088d Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Wed, 8 Jul 2026 23:20:47 +0200 Subject: [PATCH 3/8] Remove unreachable static member-select branch in dist frozen gating --- src/V3Randomize.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index beb7b2469..82b0bd8fb 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -4605,11 +4605,6 @@ class RandomizeVisitor final : public VNVisitor { 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 From 0e09102fb6bda6e2e47a171d62e6e71f7dcd83ba Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Thu, 9 Jul 2026 09:36:35 +0200 Subject: [PATCH 4/8] Cover dist dedup arms and drop unreachable chain guard --- src/V3Randomize.cpp | 1 - .../t/t_constraint_dist_randmode_frozen.v | 55 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index 82b0bd8fb..e617cca36 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -4566,7 +4566,6 @@ class RandomizeVisitor final : public VNVisitor { // frozen variable requires only membership, not a freshly drawn bucket. AstNode* gateFrozenDist(AstNode* chainp, AstDist* distp, const std::vector& buckets, AstVar* randModeVarp) { - if (!chainp) return chainp; FileLine* const fl = distp->fileline(); AstNodeExpr* gatep = nullptr; std::unordered_set gatedVars; diff --git a/test_regress/t/t_constraint_dist_randmode_frozen.v b/test_regress/t/t_constraint_dist_randmode_frozen.v index 461296871..6dc84b138 100644 --- a/test_regress/t/t_constraint_dist_randmode_frozen.v +++ b/test_regress/t/t_constraint_dist_randmode_frozen.v @@ -82,6 +82,19 @@ class MultiVar; 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; @@ -259,6 +272,48 @@ module t; `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; From a551b9b693dd4449094f6573e95d783fc2b1abd8 Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Sun, 12 Jul 2026 23:03:59 +0200 Subject: [PATCH 5/8] Extract weighted-bucket-chain helpers and merge frozen-dist gate traversal --- src/V3Randomize.cpp | 174 ++++++++++++++++++++++---------------------- 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index e617cca36..4623dae4e 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -4586,31 +4586,31 @@ class RandomizeVisitor final : public VNVisitor { if (!smodep) return nullptr; return new AstVarRef{fl, VN_AS(smodep->user2p(), NodeModule), smodep, VAccess::READ}; }; - // Direct var references and sub-object member references each carry a mode bit. - distp->exprp()->foreach([&](const AstNodeVarRef* refp) { - AstVar* const varp = refp->varp(); + // Each direct, static, or sub-object member reference carries a mode bit. + distp->exprp()->foreach([&](const AstNode* nodep) { + const AstNodeVarRef* const refp = VN_CAST(nodep, NodeVarRef); + const AstMemberSel* const mselp = refp ? nullptr : VN_CAST(nodep, MemberSel); + if (!refp && !mselp) return; + AstVar* const varp = refp ? refp->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); + } else if (mselp) { + 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); } 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; - 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 (!gatep) return chainp; AstNodeExpr* unionExprp = nullptr; @@ -4636,6 +4636,78 @@ class RandomizeVisitor final : public VNVisitor { return new AstConstraintIf{fl, gatep, chainp, new AstConstraintExpr{fl, unionExprp}}; } + AstNodeExpr* newUniformRangePick(AstDist* distp, const AstInsideRange* irp) { + FileLine* const fl = distp->fileline(); + AstNodeExpr* const distExprCopyp = distp->exprp()->cloneTreePure(false); + distExprCopyp->user1(true); + const int distWidth = distp->exprp()->width(); + const AstConst* const lopC = VN_CAST(irp->lhsp(), Const); + const AstConst* const hipC = VN_CAST(irp->rhsp(), Const); + AstNodeExpr* rangeSzp; + if (lopC && hipC) { + const uint64_t rsz = hipC->toUQuad() - lopC->toUQuad() + 1; + rangeSzp = new AstConst{fl, AstConst::Unsized64{}, rsz}; + } else { + const bool isSigned = irp->lhsp()->isSigned(); + AstNodeExpr* const lo64p + = isSigned ? static_cast( + new AstExtendS{fl, irp->lhsp()->cloneTreePure(false), 64}) + : static_cast( + new AstExtend{fl, irp->lhsp()->cloneTreePure(false), 64}); + lo64p->dtypeSetUInt64(); + AstNodeExpr* const hi64p + = isSigned ? static_cast( + new AstExtendS{fl, irp->rhsp()->cloneTreePure(false), 64}) + : static_cast( + new AstExtend{fl, irp->rhsp()->cloneTreePure(false), 64}); + hi64p->dtypeSetUInt64(); + rangeSzp = new AstAdd{fl, new AstConst{fl, AstConst::Unsized64{}, 1ULL}, + new AstSub{fl, hi64p, lo64p}}; + } + AstNodeExpr* const rand64p = new AstRand{fl, nullptr, false}; + rand64p->dtypeSetUInt64(); + AstNodeExpr* const offsetp + = new AstCCast{fl, new AstModDiv{fl, rand64p, rangeSzp}, distWidth}; + AstNodeExpr* const valuep = new AstAdd{fl, irp->lhsp()->cloneTreePure(false), offsetp}; + valuep->dtypeFrom(distp->exprp()); + AstNodeExpr* const eqp = new AstEq{fl, distExprCopyp, valuep}; + eqp->user1(true); + return eqp; + } + + // Soft weighted bucket chain: select a bucket by bucketVar against cumulative weights. + AstNode* buildWeightedBucketChain(AstDist* distp, const std::vector& buckets, + AstVar* bucketVarp, + const std::vector& cumSums) { + FileLine* const fl = distp->fileline(); + AstNode* chainp = nullptr; + for (int i = static_cast(buckets.size()) - 1; i >= 0; --i) { + AstNodeExpr* constraintExprp; + const AstInsideRange* const irp = VN_CAST(buckets[i].rangep, InsideRange); + if (irp && (distBoundRefsRandVar(irp->lhsp()) || distBoundRefsRandVar(irp->rhsp()))) { + constraintExprp = newDistRangeMembership(distp, irp); + } else if (irp) { + constraintExprp = newUniformRangePick(distp, irp); + } else { + AstNodeExpr* const distExprCopyp = distp->exprp()->cloneTreePure(false); + distExprCopyp->user1(true); + constraintExprp + = new AstEq{fl, distExprCopyp, buckets[i].rangep->cloneTreePure(false)}; + constraintExprp->user1(true); + } + AstConstraintExpr* const thenp = new AstConstraintExpr{fl, constraintExprp}; + thenp->isSoft(true); + 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}; + } + } + return chainp; + } + // Replace AstDist with weighted bucket selection via AstConstraintIf chain. // Supports both constant and variable weight expressions. void lowerDistConstraints(AstTask* taskp, AstNode* constrItemsp, AstVar* randModeVarp, @@ -4770,79 +4842,7 @@ class RandomizeVisitor final : public VNVisitor { 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; - const AstInsideRange* const irp = VN_CAST(buckets[i].rangep, InsideRange); - if (irp - && (distBoundRefsRandVar(irp->lhsp()) || distBoundRefsRandVar(irp->rhsp()))) { - // Bounds solved concurrently cannot pin a pre-solve value; softly - // prefer the symbolic range so the hard membership stays satisfiable - constraintExprp = newDistRangeMembership(distp, irp); - } else if (irp) { - // Pick distExpr = lo + rand64() % (hi - lo + 1) for a uniform value in range - AstNodeExpr* const distExprCopyp = distp->exprp()->cloneTreePure(false); - distExprCopyp->user1(true); - const int distWidth = distp->exprp()->width(); - // Compute range size in 64-bit to avoid overflow - const AstConst* const lopC = VN_CAST(irp->lhsp(), Const); - const AstConst* const hipC = VN_CAST(irp->rhsp(), Const); - AstNodeExpr* rangeSzp; - if (lopC && hipC) { - const uint64_t rsz = hipC->toUQuad() - lopC->toUQuad() + 1; - rangeSzp = new AstConst{fl, AstConst::Unsized64{}, rsz}; - } else { - const bool isSigned = irp->lhsp()->isSigned(); - AstNodeExpr* const lo64p - = isSigned - ? static_cast( - new AstExtendS{fl, irp->lhsp()->cloneTreePure(false), 64}) - : static_cast( - new AstExtend{fl, irp->lhsp()->cloneTreePure(false), 64}); - lo64p->dtypeSetUInt64(); - AstNodeExpr* const hi64p - = isSigned - ? static_cast( - new AstExtendS{fl, irp->rhsp()->cloneTreePure(false), 64}) - : static_cast( - new AstExtend{fl, irp->rhsp()->cloneTreePure(false), 64}); - hi64p->dtypeSetUInt64(); - rangeSzp = new AstAdd{fl, new AstConst{fl, AstConst::Unsized64{}, 1ULL}, - new AstSub{fl, hi64p, lo64p}}; - } - AstNodeExpr* const rand64p = new AstRand{fl, nullptr, false}; - rand64p->dtypeSetUInt64(); - // offset = rand64() % rangeSize (result in [0, rangeSize-1]) - AstNodeExpr* const offset64p = new AstModDiv{fl, rand64p, rangeSzp}; - // Truncate offset to dist expression width, then add lo - AstNodeExpr* const offsetp = new AstCCast{fl, offset64p, distWidth}; - AstNodeExpr* const lop = irp->lhsp()->cloneTreePure(false); - AstNodeExpr* const valuep = new AstAdd{fl, lop, offsetp}; - valuep->dtypeFrom(distp->exprp()); - constraintExprp = new AstEq{fl, distExprCopyp, valuep}; - constraintExprp->user1(true); - } else { - AstNodeExpr* const distExprCopyp = distp->exprp()->cloneTreePure(false); - distExprCopyp->user1(true); - constraintExprp - = new AstEq{fl, distExprCopyp, buckets[i].rangep->cloneTreePure(false)}; - constraintExprp->user1(true); - } - - AstConstraintExpr* const thenp = new AstConstraintExpr{fl, constraintExprp}; - // Per IEEE 18.5.3: weights are a preference, not a hard constraint. - // The solver may discard this when it conflicts with other constraints. - thenp->isSoft(true); - - 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}; - } - } + AstNode* chainp = buildWeightedBucketChain(distp, buckets, bucketVarp, cumSums); chainp = gateFrozenDist(chainp, distp, buckets, randModeVarp); From 50c105937449fa4f315014779cfc7b9d559c2933 Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Mon, 13 Jul 2026 22:35:37 +0200 Subject: [PATCH 6/8] address review: brace, exist, dtype and test --- src/V3Randomize.cpp | 26 ++-- .../t/t_constraint_dist_randmode_frozen.v | 115 +++++++++++++++--- 2 files changed, 109 insertions(+), 32 deletions(-) diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index 4623dae4e..c7deebfa5 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -4488,10 +4488,8 @@ class RandomizeVisitor final : public VNVisitor { 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}); @@ -4534,8 +4532,9 @@ class RandomizeVisitor final : public VNVisitor { // Membership test for one bucket: a range comparison or an equality. static AstNodeExpr* newDistMembershipTerm(AstDist* distp, AstNodeExpr* rangep) { - if (const AstInsideRange* const irp = VN_CAST(rangep, InsideRange)) + if (const AstInsideRange* const irp = VN_CAST(rangep, InsideRange)) { return newDistRangeMembership(distp, irp); + } FileLine* const fl = distp->fileline(); AstNodeExpr* const eqExprp = distp->exprp()->cloneTreePure(false); eqExprp->user1(true); @@ -4589,14 +4588,15 @@ class RandomizeVisitor final : public VNVisitor { // Each direct, static, or sub-object member reference carries a mode bit. distp->exprp()->foreach([&](const AstNode* nodep) { const AstNodeVarRef* const refp = VN_CAST(nodep, NodeVarRef); - const AstMemberSel* const mselp = refp ? nullptr : VN_CAST(nodep, MemberSel); + const AstMemberSel* const mselp = VN_CAST(nodep, MemberSel); if (!refp && !mselp) return; AstVar* const varp = refp ? refp->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)) + if (AstNodeExpr* const readp = staticModeRead(varp)) { addModeGate(readp, rmode.index); + } } else if (mselp) { AstVar* const memberModep = getRandModeVarFromClass(VN_AS(varp->user2p(), NodeModule)); @@ -4617,8 +4617,8 @@ class RandomizeVisitor final : public VNVisitor { for (auto& bucket : buckets) { AstNodeExpr* termp = newDistMembershipTerm(distp, bucket.rangep); // A weight that is zero only at runtime excludes the bucket's values too. - bool runtimeWeight = false; - bucket.weightExprp->foreach([&](const AstNodeVarRef*) { runtimeWeight = true; }); + 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}}; @@ -4654,13 +4654,11 @@ class RandomizeVisitor final : public VNVisitor { new AstExtendS{fl, irp->lhsp()->cloneTreePure(false), 64}) : static_cast( new AstExtend{fl, irp->lhsp()->cloneTreePure(false), 64}); - lo64p->dtypeSetUInt64(); AstNodeExpr* const hi64p = isSigned ? static_cast( new AstExtendS{fl, irp->rhsp()->cloneTreePure(false), 64}) : static_cast( new AstExtend{fl, irp->rhsp()->cloneTreePure(false), 64}); - hi64p->dtypeSetUInt64(); rangeSzp = new AstAdd{fl, new AstConst{fl, AstConst::Unsized64{}, 1ULL}, new AstSub{fl, hi64p, lo64p}}; } @@ -4669,7 +4667,6 @@ class RandomizeVisitor final : public VNVisitor { AstNodeExpr* const offsetp = new AstCCast{fl, new AstModDiv{fl, rand64p, rangeSzp}, distWidth}; AstNodeExpr* const valuep = new AstAdd{fl, irp->lhsp()->cloneTreePure(false), offsetp}; - valuep->dtypeFrom(distp->exprp()); AstNodeExpr* const eqp = new AstEq{fl, distExprCopyp, valuep}; eqp->user1(true); return eqp; @@ -4735,10 +4732,12 @@ class RandomizeVisitor final : public VNVisitor { // dist can appear inside an if/else or foreach constraint. if (AstConstraintIf* const cifp = VN_CAST(itemp, ConstraintIf)) { - if (cifp->thensp()) + if (cifp->thensp()) { lowerDistConstraints(taskp, cifp->thensp(), randModeVarp, foreachp); - if (cifp->elsesp()) + } + if (cifp->elsesp()) { lowerDistConstraints(taskp, cifp->elsesp(), randModeVarp, foreachp); + } continue; } if (AstConstraintForeach* const cfep = VN_CAST(itemp, ConstraintForeach)) { @@ -4940,8 +4939,9 @@ class RandomizeVisitor final : public VNVisitor { } if (constrp->itemsp()) expandUniqueElementList(constrp->itemsp()); - if (constrp->itemsp()) + if (constrp->itemsp()) { lowerDistConstraints(taskp, constrp->itemsp(), randModeVarp); + } std::set& sizeArrays = m_sizeConstrainedArrays[classp]; ConstraintExprVisitor{classp, m_memberMap, constrp->itemsp(), nullptr, genp, randModeVarp, diff --git a/test_regress/t/t_constraint_dist_randmode_frozen.v b/test_regress/t/t_constraint_dist_randmode_frozen.v index 6dc84b138..396327658 100644 --- a/test_regress/t/t_constraint_dist_randmode_frozen.v +++ b/test_regress/t/t_constraint_dist_randmode_frozen.v @@ -15,43 +15,75 @@ typedef int unsigned uint; class Base; rand uint delay; rand uint x; - constraint delay_c {delay inside {[1:100]};} + 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};} + constraint c { + x dist { + 5 := 99, + 1000000 := 1 + }; + } endclass class RangeVals extends Base; - constraint c {x dist {[1:10] := 1, [1000:2000] := 9};} + 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};} + 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; + 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};} + 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};} + constraint delay_c {delay inside {[1 : 100]};} + constraint c { + sx dist { + 5 := 9, + 70 := 1 + }; + } function int rd(); return this.randomize(delay); endfunction @@ -60,7 +92,7 @@ endclass class StaticPlain; // non-dist constraint on a frozen static var rand uint d; static rand uint sy; - constraint c {sy inside {[10:20]};} + constraint c {sy inside {[10 : 20]};} endclass class Sub; @@ -70,8 +102,13 @@ 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};} + constraint delay_c {delay inside {[1 : 100]};} + constraint c { + s.x dist { + 5 := 9, + 70 := 1 + }; + } function new(); s = new; endfunction @@ -79,17 +116,32 @@ endclass class MultiVar; rand uint x, y; - constraint c {(x + y) dist {10 := 9, 1000 := 1};} + constraint c { + (x + y) dist { + 10 := 9, + 1000 := 1 + }; + } endclass class DupVar; rand uint x; - constraint c {(x + x) dist {10 := 9, 20 := 1};} + 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};} + constraint c { + (s.x + s.x) dist { + 10 := 9, + 20 := 1 + }; + } function new(); s = new; endfunction @@ -98,7 +150,12 @@ endclass class RtWeight; rand uint x; uint w; - constraint c {x dist {5 := 9, 70 := w};} + constraint c { + x dist { + 5 := 9, + 70 := w + }; + } endclass module t; @@ -124,6 +181,7 @@ module t; ok = sv.rd(); `checkd(ok, 1); `checkd(sv.x, 1000000); + if (sv.delay < 1 || sv.delay > 100) `checkd(sv.delay, 0); end // Frozen in the high-weight bucket -> succeeds. @@ -246,6 +304,25 @@ module t; `checkd(h.s.x, 7); 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; @@ -350,7 +427,8 @@ module t; `checkd(ok, 1); if (di.sel == 1) begin if (di.x != 5 && di.x != 70) `checkd(0, 1); - end else begin + end + else begin `checkd(di.x, 4242); end end @@ -360,8 +438,7 @@ module t; 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) `checkd(0, 1); + foreach (df.arr[j]) if (df.arr[j] != 10 && df.arr[j] != 20) `checkd(0, 1); end $write("*-* All Finished *-*\n"); From 71334a92342f19a5e172d0b95d7406feea1331a7 Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Tue, 14 Jul 2026 15:02:47 +0200 Subject: [PATCH 7/8] Gate dist by whether the expression value can still be drawn, fixing frozen-index weights --- src/V3Randomize.cpp | 25 +++++++++++++----- .../t/t_constraint_dist_randmode_frozen.v | 26 +++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index c7deebfa5..41d963b60 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -4561,19 +4561,25 @@ class RandomizeVisitor final : public VNVisitor { return new AstConstraintExpr{fl, unionExprp}; } - // Gate the weighted chain on the rand_mode bits of the dist's variables, so a - // frozen variable requires only membership, not a freshly drawn bucket. + // Weighted chain while any value-source variable is active, membership once + // all are frozen. AstNode* gateFrozenDist(AstNode* chainp, AstDist* distp, const std::vector& buckets, AstVar* randModeVarp) { FileLine* const fl = distp->fileline(); AstNodeExpr* gatep = nullptr; std::unordered_set gatedVars; + // Handles are never re-pointed by randomize(), only read through. + std::unordered_set handleVars; + distp->exprp()->foreach([&](const AstMemberSel* mselp) { + mselp->fromp()->foreach( + [&](const AstNodeVarRef* hrefp) { handleVars.insert(hrefp->varp()); }); + }); 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 = new AstLogOr{fl, gatep, atp}; gatep->dtypeSetBit(); } else { gatep = atp; @@ -4586,13 +4592,21 @@ class RandomizeVisitor final : public VNVisitor { return new AstVarRef{fl, VN_AS(smodep->user2p(), NodeModule), smodep, VAccess::READ}; }; // Each direct, static, or sub-object member reference carries a mode bit. + // A rand variable without a mode bit can never freeze, so no gate is needed. + bool alwaysActive = false; distp->exprp()->foreach([&](const AstNode* nodep) { const AstNodeVarRef* const refp = VN_CAST(nodep, NodeVarRef); const AstMemberSel* const mselp = VN_CAST(nodep, MemberSel); if (!refp && !mselp) return; AstVar* const varp = refp ? refp->varp() : mselp->varp(); + if (refp && handleVars.count(varp)) return; + if (!varp->rand().isRandomizable()) return; const RandomizeMode rmode = {.asInt = varp->user1()}; - if (!rmode.usesMode || !gatedVars.insert(varp).second) return; + if (!rmode.usesMode) { + alwaysActive = true; + return; + } + if (!gatedVars.insert(varp).second) return; if (varp->lifetime().isStatic()) { if (AstNodeExpr* const readp = staticModeRead(varp)) { addModeGate(readp, rmode.index); @@ -4603,7 +4617,6 @@ class RandomizeVisitor final : public VNVisitor { if (!memberModep) return; AstMemberSel* const modeSelp = new AstMemberSel{fl, mselp->fromp()->cloneTreePure(false), memberModep}; - modeSelp->dtypep(memberModep->dtypep()); addModeGate(modeSelp, rmode.index); } else if (randModeVarp) { addModeGate(new AstVarRef{fl, VN_AS(randModeVarp->user2p(), NodeModule), @@ -4611,7 +4624,7 @@ class RandomizeVisitor final : public VNVisitor { rmode.index); } }); - if (!gatep) return chainp; + if (alwaysActive || !gatep) return chainp; AstNodeExpr* unionExprp = nullptr; for (auto& bucket : buckets) { diff --git a/test_regress/t/t_constraint_dist_randmode_frozen.v b/test_regress/t/t_constraint_dist_randmode_frozen.v index 396327658..7971a2672 100644 --- a/test_regress/t/t_constraint_dist_randmode_frozen.v +++ b/test_regress/t/t_constraint_dist_randmode_frozen.v @@ -158,6 +158,18 @@ class RtWeight; } 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 + module t; int ok; initial begin @@ -170,6 +182,7 @@ module t; RtWeight r; DistInIf di; DistInForeach df; + IdxSel ix; // Frozen in the low-weight bucket -> succeeds, value untouched. sv = new; @@ -441,6 +454,19 @@ module t; foreach (df.arr[j]) if (df.arr[j] != 10 && df.arr[j] != 20) `checkd(0, 1); end + // Frozen index into an active array: a[idx] is still freshly drawn. + ix = new; + ok = ix.randomize(); + `checkd(ok, 1); + ix.idx.rand_mode(0); + ix.idx = 2; + for (int i = 0; i < 16; ++i) begin + ok = ix.randomize(); + `checkd(ok, 1); + `checkd(ix.idx, 2); + if (ix.a[2] != 10 && ix.a[2] != 20) `checkd(ix.a[2], 10); + end + $write("*-* All Finished *-*\n"); $finish; end From 8e6fab307581088ab4731558414e1e243f4ba22f Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Tue, 14 Jul 2026 15:30:32 +0200 Subject: [PATCH 8/8] Delete orphaned gate expression when dist gating is skipped --- src/V3Randomize.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index 41d963b60..a50292ddc 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -4624,7 +4624,10 @@ class RandomizeVisitor final : public VNVisitor { rmode.index); } }); - if (alwaysActive || !gatep) return chainp; + if (alwaysActive || !gatep) { + if (gatep) VL_DO_DANGLING(pushDeletep(gatep), gatep); + return chainp; + } AstNodeExpr* unionExprp = nullptr; for (auto& bucket : buckets) {