From cd8818ca23df7056994449c5e8e978aa8413a90d Mon Sep 17 00:00:00 2001 From: Rahul Behl Date: Wed, 11 Mar 2026 16:21:32 +0530 Subject: [PATCH] Fix randomize size+element queue constraints (#5582) (#7225) --- include/verilated_random.h | 57 +++++-- src/V3AstAttr.h | 2 + src/V3Randomize.cpp | 143 ++++++++++++++++-- .../t/t_randomize_method_types_unsup.out | 8 +- .../t/t_randomize_method_types_unsup.v | 4 - .../t/t_randomize_queue_size_element.py | 21 +++ .../t/t_randomize_queue_size_element.v | 82 ++++++++++ 7 files changed, 280 insertions(+), 37 deletions(-) create mode 100755 test_regress/t/t_randomize_queue_size_element.py create mode 100644 test_regress/t/t_randomize_queue_size_element.v diff --git a/include/verilated_random.h b/include/verilated_random.h index 010a52a9f..09e12adad 100644 --- a/include/verilated_random.h +++ b/include/verilated_random.h @@ -88,6 +88,7 @@ public: m_arrVarsRefp = arrVarsRefp; } mutable std::map count_cache; + void clearCountCache() const { count_cache.clear(); } int countMatchingElements(const ArrayInfoMap& arr_vars, const std::string& base_name) const { if (VL_LIKELY(count_cache.find(base_name) != count_cache.end())) return count_cache[base_name]; @@ -172,7 +173,14 @@ public: for (int i = 0; i < dimension(); ++i) s << ")"; } } else { - VL_FATAL_MT(__FILE__, __LINE__, "randomize", "indexed_name not found in m_arr_vars"); + if (dimension() > 0) { + for (int i = 0; i < dimension(); ++i) s << "(Array (_ BitVec 32) "; + s << "(_ BitVec " << width() << ")"; + for (int i = 0; i < dimension(); ++i) s << ")"; + } else { + VL_FATAL_MT(__FILE__, __LINE__, "randomize", + "indexed_name not found in m_arr_vars"); + } } } int totalWidth() const override { @@ -355,12 +363,15 @@ public: typename std::enable_if::value, void>::type write_var(VlQueue& var, int width, const char* name, int dimension, std::uint32_t randmodeIdx = std::numeric_limits::max()) { - if (m_vars.find(name) != m_vars.end()) return; - m_vars[name] = std::make_shared>>( - name, width, &var, dimension, randmodeIdx); + if (m_vars.find(name) == m_vars.end()) { + m_vars[name] = std::make_shared>>( + name, width, &var, dimension, randmodeIdx); + } if (dimension > 0) { m_index = 0; + clear_arr_table(name); record_arr_table(var, name, dimension, {}, {}); + m_vars[name]->clearCountCache(); } } @@ -377,15 +388,17 @@ public: write_var(VlUnpacked& var, uint32_t width, const std::string& name, uint32_t dimension, std::uint32_t randmodeIdx = std::numeric_limits::max()) { - - if (m_vars.find(name) != m_vars.end()) return; - - m_vars[name] = std::make_shared>>( - name, width, &var, dimension, randmodeIdx); + if (m_vars.find(name) == m_vars.end()) { + m_vars[name] + = std::make_shared>>( + name, width, &var, dimension, randmodeIdx); + } if (dimension > 0) { m_index = 0; + clear_arr_table(name); record_arr_table(var, name, dimension, {}, {}); + m_vars[name]->clearCountCache(); } } // Register unpacked array of structs @@ -401,13 +414,16 @@ public: typename std::enable_if::value, void>::type write_var(VlAssocArray& var, int width, const char* name, int dimension, std::uint32_t randmodeIdx = std::numeric_limits::max()) { - if (m_vars.find(name) != m_vars.end()) return; - m_vars[name] - = std::make_shared>>( - name, width, &var, dimension, randmodeIdx); + if (m_vars.find(name) == m_vars.end()) { + m_vars[name] + = std::make_shared>>( + name, width, &var, dimension, randmodeIdx); + } if (dimension > 0) { m_index = 0; + clear_arr_table(name); record_arr_table(var, name, dimension, {}, {}); + m_vars[name]->clearCountCache(); } } @@ -592,10 +608,25 @@ public: + std::to_string(idx); } + // Helper: Clear existing array element entries for a base name + void clear_arr_table(const std::string& name) { + for (int index = 0;; ++index) { + const std::string key = generateKey(name, index); + const auto it = m_arr_vars.find(key); + if (it == m_arr_vars.end()) break; + m_arr_vars.erase(it); + } + } + void hard(std::string&& constraint, const char* filename = "", uint32_t linenum = 0, const char* source = ""); void soft(std::string&& constraint, const char* filename = "", uint32_t linenum = 0, const char* source = ""); + void pin_var(const char* name, int width, uint64_t value) { + std::string constraint = "(__Vbv (= "s + name + " (_ bv" + std::to_string(value) + " " + + std::to_string(width) + ")))"; + hard(std::move(constraint)); + } void disable_soft(const std::string& varName); void clearConstraints(); void clearAll(); // Clear both constraints and variables diff --git a/src/V3AstAttr.h b/src/V3AstAttr.h index bbaa9c1ec..49a6554f9 100644 --- a/src/V3AstAttr.h +++ b/src/V3AstAttr.h @@ -821,6 +821,7 @@ public: RANDOMIZER_UNIQUE, RANDOMIZER_MARK_RANDC, RANDOMIZER_SOLVE_BEFORE, + RANDOMIZER_PIN_VAR, RANDOMIZER_WRITE_VAR, RNG_GET_RANDSTATE, RNG_SET_RANDSTATE, @@ -959,6 +960,7 @@ inline std::ostream& operator<<(std::ostream& os, const VCMethod& rhs) { {RANDOMIZER_UNIQUE, "rand_unique", false}, \ {RANDOMIZER_MARK_RANDC, "markRandc", false}, \ {RANDOMIZER_SOLVE_BEFORE, "solveBefore", false}, \ + {RANDOMIZER_PIN_VAR, "pin_var", false}, \ {RANDOMIZER_WRITE_VAR, "write_var", false}, \ {RNG_GET_RANDSTATE, "__Vm_rng.get_randstate", true}, \ {RNG_SET_RANDSTATE, "__Vm_rng.set_randstate", false}, \ diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index 2ec2a97cf..8e4c9d51d 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -740,6 +740,7 @@ class ConstraintExprVisitor final : public VNVisitor { // (used to format "%@.%@" for struct arrays) std::set& m_writtenVars; // Track which variable paths have write_var generated // (shared across all constraints) + std::set* m_sizeConstrainedArraysp = nullptr; // Arrays with size+element constraints // Build full path for a MemberSel chain (e.g., "obj.l2.l3.l4") std::string buildMemberPath(const AstMemberSel* const memberSelp) { @@ -953,9 +954,14 @@ class ConstraintExprVisitor final : public VNVisitor { void visit(AstNodeVarRef* nodep) override { AstVar* varp = nodep->varp(); if (varp->user4p()) { - varp->user4p()->v3warn( - CONSTRAINTIGN, - "Size constraint combined with element constraint may not work correctly"); + bool isSizeRef = false; + if (AstCMethodHard* const methodp = VN_CAST(nodep->backp(), CMethodHard)) { + if (methodp->method() == VCMethod::ASSOC_SIZE + || methodp->method() == VCMethod::DYN_SIZE) { + isSizeRef = true; + } + } + if (!isSizeRef && m_sizeConstrainedArraysp) { m_sizeConstrainedArraysp->insert(varp); } } // Check if this variable is marked as globally constrained @@ -2394,14 +2400,16 @@ public: explicit ConstraintExprVisitor(AstClass* classp, VMemberMap& memberMap, AstNode* nodep, AstNodeFTask* inlineInitTaskp, AstVar* genp, AstVar* randModeVarp, std::set& writtenVars, - AstNodeFTask* memberselInitTaskp = nullptr) + AstNodeFTask* memberselInitTaskp = nullptr, + std::set* sizeConstrainedArraysp = nullptr) : m_classp{classp} , m_inlineInitTaskp{inlineInitTaskp} , m_memberselInitTaskp{memberselInitTaskp} , m_genp{genp} , m_randModeVarp{randModeVarp} , m_memberMap{memberMap} - , m_writtenVars{writtenVars} { + , m_writtenVars{writtenVars} + , m_sizeConstrainedArraysp{sizeConstrainedArraysp} { iterateAndNextNull(nodep); } }; @@ -2699,6 +2707,7 @@ class RandomizeVisitor final : public VNVisitor { 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 + std::map> m_sizeConstrainedArrays; // Per-class arrays std::map m_staticConstraintModeVars; // Static constraint mode vars per class @@ -3985,8 +3994,10 @@ 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, randomizep}; + std::set& sizeArrays = m_sizeConstrainedArrays[classp]; + ConstraintExprVisitor{classp, m_memberMap, constrp->itemsp(), + nullptr, genp, randModeVarp, + m_writtenVars, randomizep, &sizeArrays}; if (constrp->itemsp()) { taskp->addStmtsp(wrapIfConstraintMode( nodep, constrp, constrp->itemsp()->unlinkFrBackWithNext())); @@ -4090,7 +4101,106 @@ class RandomizeVisitor final : public VNVisitor { solverCallp->dtypeSetBit(); solverCallp->add(new AstVarRef{fl, genModp, genp, VAccess::READWRITE}); solverCallp->add(".next(__Vm_rng)"); - beginValp = solverCallp; + const auto sizeArraysIt = m_sizeConstrainedArrays.find(nodep); + const bool needsSizePhase + = sizeArraysIt != m_sizeConstrainedArrays.end() && !sizeArraysIt->second.empty(); + if (needsSizePhase) { + AstVar* const sizeOkVarp = new AstVar{fl, VVarType::BLOCKTEMP, "__Vsize_ok", + nodep->findBasicDType(VBasicDTypeKwd::BIT)}; + sizeOkVarp->funcLocal(true); + randomizep->addStmtsp(sizeOkVarp); + + AstVar* const finalOkVarp = new AstVar{fl, VVarType::BLOCKTEMP, "__Vfinal_ok", + nodep->findBasicDType(VBasicDTypeKwd::BIT)}; + finalOkVarp->funcLocal(true); + randomizep->addStmtsp(finalOkVarp); + + // First pass: solve size variables (and other constraints) to determine sizes + randomizep->addStmtsp( + new AstAssign{fl, new AstVarRef{fl, sizeOkVarp, VAccess::WRITE}, solverCallp}); + + // Resize constrained arrays before rebuilding constraints + if (AstTask* const resizeAllTaskp + = VN_AS(m_memberMap.findMember(nodep, "__Vresize_constrained_arrays"), Task)) { + AstTaskRef* const resizeTaskRefp = new AstTaskRef{fl, resizeAllTaskp}; + AstIf* const ifp = new AstIf{fl, new AstVarRef{fl, sizeOkVarp, VAccess::READ}, + resizeTaskRefp->makeStmt(), nullptr}; + randomizep->addStmtsp(ifp); + } + + // Refresh array element tables after resize + for (AstVar* const arrVarp : sizeArraysIt->second) { + AstCMethodHard* const methodp = new AstCMethodHard{ + fl, new AstVarRef{fl, genModp, genp, VAccess::READWRITE}, + VCMethod::RANDOMIZER_WRITE_VAR}; + methodp->dtypeSetVoid(); + + AstNodeModule* const classp = VN_AS(arrVarp->user2p(), NodeModule); + AstVarRef* const varRefp = new AstVarRef{fl, classp, arrVarp, VAccess::WRITE}; + varRefp->classOrPackagep(classp); + methodp->addPinsp(varRefp); + + uint32_t dimension = 0; + if (VN_IS(arrVarp->dtypep(), UnpackArrayDType) + || VN_IS(arrVarp->dtypep(), DynArrayDType) + || VN_IS(arrVarp->dtypep(), QueueDType) + || VN_IS(arrVarp->dtypep(), AssocArrayDType)) { + const std::pair dims + = arrVarp->dtypep()->dimensions(/*includeBasic=*/true); + dimension = dims.second; + } + + AstNodeDType* tmpDtypep = arrVarp->dtypep(); + while (VN_IS(tmpDtypep, UnpackArrayDType) || VN_IS(tmpDtypep, DynArrayDType) + || VN_IS(tmpDtypep, QueueDType) || VN_IS(tmpDtypep, AssocArrayDType)) + tmpDtypep = tmpDtypep->subDTypep(); + const size_t width = tmpDtypep->width(); + + methodp->addPinsp(new AstConst{fl, AstConst::Unsized64{}, width}); + AstNodeExpr* const varnamep = new AstCExpr{ + fl, AstCExpr::Pure{}, "\"" + arrVarp->name() + "\"", arrVarp->width()}; + varnamep->dtypep(arrVarp->dtypep()); + methodp->addPinsp(varnamep); + methodp->addPinsp(new AstConst{fl, AstConst::Unsized64{}, dimension}); + + randomizep->addStmtsp(methodp->makeStmt()); + } + + // Rebuild constraints after resize and pin size variables + randomizep->addStmtsp(implementConstraintsClear(fl, genp)); + AstTaskRef* const setupTaskRefp2 = new AstTaskRef{fl, setupAllTaskp}; + randomizep->addStmtsp(setupTaskRefp2->makeStmt()); + + for (AstVar* const arrVarp : sizeArraysIt->second) { + AstVar* const sizeVarp = VN_CAST(arrVarp->user4p(), Var); + if (!sizeVarp) continue; + AstCMethodHard* const pinp = new AstCMethodHard{ + fl, new AstVarRef{fl, genModp, genp, VAccess::READWRITE}, + VCMethod::RANDOMIZER_PIN_VAR}; + pinp->dtypeSetVoid(); + AstCExpr* const namep + = new AstCExpr{fl, AstCExpr::Pure{}, "\"" + sizeVarp->name() + "\""}; + namep->dtypeSetUInt32(); + pinp->addPinsp(namep); + pinp->addPinsp(new AstConst{fl, AstConst::Unsized64{}, + static_cast(sizeVarp->width())}); + pinp->addPinsp(new AstVarRef{fl, sizeVarp, VAccess::READ}); + randomizep->addStmtsp(pinp->makeStmt()); + } + + // Final pass: solve full constraints with sizes pinned + AstCExpr* const solverCallp2 = new AstCExpr{fl}; + solverCallp2->dtypeSetBit(); + solverCallp2->add(new AstVarRef{fl, genModp, genp, VAccess::READWRITE}); + solverCallp2->add(".next(__Vm_rng)"); + randomizep->addStmtsp(new AstAssign{ + fl, new AstVarRef{fl, finalOkVarp, VAccess::WRITE}, solverCallp2}); + + beginValp = new AstAnd{fl, new AstVarRef{fl, sizeOkVarp, VAccess::READ}, + new AstVarRef{fl, finalOkVarp, VAccess::READ}}; + } else { + beginValp = solverCallp; + } if (randModeVarp) { AstNodeModule* const randModeClassp = VN_AS(randModeVarp->user2p(), Class); AstNodeFTask* const newp @@ -4105,10 +4215,15 @@ class RandomizeVisitor final : public VNVisitor { AstVarRef* const fvarRefp = new AstVarRef{fl, fvarp, VAccess::WRITE}; randomizep->addStmtsp(new AstAssign{fl, fvarRefp, beginValp}); - if (AstTask* const resizeAllTaskp - = VN_AS(m_memberMap.findMember(nodep, "__Vresize_constrained_arrays"), Task)) { - AstTaskRef* const resizeTaskRefp = new AstTaskRef{fl, resizeAllTaskp}; - randomizep->addStmtsp(resizeTaskRefp->makeStmt()); + const auto sizeArraysIt = m_sizeConstrainedArrays.find(nodep); + const bool needsSizePhase + = sizeArraysIt != m_sizeConstrainedArrays.end() && !sizeArraysIt->second.empty(); + if (!needsSizePhase) { + if (AstTask* const resizeAllTaskp + = VN_AS(m_memberMap.findMember(nodep, "__Vresize_constrained_arrays"), Task)) { + AstTaskRef* const resizeTaskRefp = new AstTaskRef{fl, resizeAllTaskp}; + randomizep->addStmtsp(resizeTaskRefp->makeStmt()); + } } AstVarRef* const fvarRefReadp = fvarRefp->cloneTree(false); @@ -4321,7 +4436,7 @@ class RandomizeVisitor final : public VNVisitor { { expandUniqueElementList(capturedTreep); ConstraintExprVisitor{nullptr, m_memberMap, capturedTreep, randomizeFuncp, - stdrand, nullptr, m_writtenVars}; + stdrand, nullptr, m_writtenVars, nullptr}; } AstCExpr* const solverCallp = new AstCExpr{fl}; solverCallp->dtypeSetBit(); @@ -4455,7 +4570,7 @@ class RandomizeVisitor final : public VNVisitor { { expandUniqueElementList(capturedTreep); ConstraintExprVisitor{classp, m_memberMap, capturedTreep, randomizeFuncp, - localGenp, randModeVarp, m_writtenVars}; + localGenp, randModeVarp, m_writtenVars, nullptr}; } // Call the solver and set return value diff --git a/test_regress/t/t_randomize_method_types_unsup.out b/test_regress/t/t_randomize_method_types_unsup.out index f9c72d91f..0697047ac 100644 --- a/test_regress/t/t_randomize_method_types_unsup.out +++ b/test_regress/t/t_randomize_method_types_unsup.out @@ -3,16 +3,12 @@ | ^~~~ ... 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. -%Warning-CONSTRAINTIGN: t/t_randomize_method_types_unsup.v:27:7: Size constraint combined with element constraint may not work correctly - : ... note: In instance 't' - 27 | q.size < 5; - | ^~~~ %Error-UNSUPPORTED: t/t_randomize_method_types_unsup.v:15:12: Unsupported: random member variable with the type of the containing class : ... note: In instance 't' 15 | rand Cls cls; | ^~~ ... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest -%Warning-CONSTRAINTIGN: t/t_randomize_method_types_unsup.v:43:41: Unsupported: randomizing this expression, treating as state - 43 | res = obj.randomize() with { dynarr.size > 2; }; +%Warning-CONSTRAINTIGN: t/t_randomize_method_types_unsup.v:39:41: Unsupported: randomizing this expression, treating as state + 39 | res = obj.randomize() with { dynarr.size > 2; }; | ^~~~ %Error: Exiting due to diff --git a/test_regress/t/t_randomize_method_types_unsup.v b/test_regress/t/t_randomize_method_types_unsup.v index 5494bbeae..de0f8ee3e 100644 --- a/test_regress/t/t_randomize_method_types_unsup.v +++ b/test_regress/t/t_randomize_method_types_unsup.v @@ -23,10 +23,6 @@ class Cls; dynarr[1].size < 10; } constraint statedep { i < st + 2; } - constraint q_size_elem { - q.size < 5; - q[i] < 10; - } constraint global_constraint { foo.x < y; } diff --git a/test_regress/t/t_randomize_queue_size_element.py b/test_regress/t/t_randomize_queue_size_element.py new file mode 100755 index 000000000..ab048b5e8 --- /dev/null +++ b/test_regress/t/t_randomize_queue_size_element.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: 2024 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_randomize_queue_size_element.v b/test_regress/t/t_randomize_queue_size_element.v new file mode 100644 index 000000000..7c6f04a25 --- /dev/null +++ b/test_regress/t/t_randomize_queue_size_element.v @@ -0,0 +1,82 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain +// SPDX-FileCopyrightText: 2026 +// SPDX-License-Identifier: CC0-1.0 + +`define check_rand(cl, field, cond) \ +begin \ + automatic longint prev_result; \ + automatic int ok; \ + if (!bit'(cl.randomize())) $stop; \ + prev_result = longint'(field); \ + if (!(cond)) $stop; \ + repeat(20) begin \ + longint result; \ + if (!bit'(cl.randomize())) $stop; \ + result = longint'(field); \ + if (!(cond)) $stop; \ + if (result != prev_result) ok = 1; \ + prev_result = result; \ + end \ + if (ok != 1) $stop; \ +end + +class SizeElemQ; + rand int q[$]; + constraint c { + q.size() > 1; + q.size() < 6; + q[0] > 100; + q[1] == q[0] + 1; + } +endclass + +class SizeElemQ2; + rand int q[$]; + rand int min_sz; + constraint c { + min_sz inside {[2:4]}; + q.size() == min_sz; + foreach (q[i]) q[i] inside {[10:20]}; + q[0] != q[q.size() - 1]; + } +endclass + +class SizeElemDynArr; + rand int da[]; + constraint c { + da.size() inside {[3:5]}; + da[0] < 50; + da[da.size() - 1] > 50; + foreach (da[i]) da[i] inside {[1:100]}; + } +endclass + +module t; + initial begin + automatic SizeElemQ obj = new; + automatic SizeElemQ2 obj2 = new; + automatic SizeElemDynArr obj3 = new; + + `check_rand(obj, obj.q[0], + obj.q.size() > 1 && obj.q.size() < 6 + && obj.q[0] > 100 + && obj.q[1] == obj.q[0] + 1); + + `check_rand(obj2, obj2.q[0], + obj2.min_sz inside {[2:4]} + && obj2.q.size() == obj2.min_sz + && obj2.q[0] inside {[10:20]} + && obj2.q[obj2.q.size() - 1] inside {[10:20]} + && obj2.q[0] != obj2.q[obj2.q.size() - 1]); + + `check_rand(obj3, obj3.da[0], + obj3.da.size() inside {[3:5]} + && obj3.da[0] < 50 + && obj3.da[obj3.da.size() - 1] > 50); + + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule