From 06907c5358ba98624aa935413f063794f424b1bd Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Sat, 4 Jul 2026 10:48:22 +0200 Subject: [PATCH] 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