From 8925762077855cc69ef402ed64fabb2be4fbf892 Mon Sep 17 00:00:00 2001 From: Yilou Wang Date: Tue, 17 Mar 2026 20:09:14 +0100 Subject: [PATCH] Fix rand_mode(0) on sub-object members not preventing solver write-back (#7272) --- include/verilated_random.cpp | 1 + include/verilated_random.h | 10 ++- src/V3AstAttr.h | 4 + src/V3LinkDot.cpp | 3 + src/V3Randomize.cpp | 69 +++++++++++++- test_regress/t/t_randomize_randmode_subobj.py | 21 +++++ test_regress/t/t_randomize_randmode_subobj.v | 90 +++++++++++++++++++ 7 files changed, 196 insertions(+), 2 deletions(-) create mode 100755 test_regress/t/t_randomize_randmode_subobj.py create mode 100644 test_regress/t/t_randomize_randmode_subobj.v diff --git a/include/verilated_random.cpp b/include/verilated_random.cpp index 59fea30a1..d7f4225b2 100644 --- a/include/verilated_random.cpp +++ b/include/verilated_random.cpp @@ -632,6 +632,7 @@ bool VlRandomizer::parseSolution(std::iostream& os, bool log) { if (m_randmodep && !varr.randModeIdxNone()) { if (!m_randmodep->at(varr.randModeIdx())) continue; } + if (m_disabledVars.count(name)) continue; if (!indices.empty()) { std::ostringstream oss; oss << varr.name(); diff --git a/include/verilated_random.h b/include/verilated_random.h index c3636e882..8fba88e2f 100644 --- a/include/verilated_random.h +++ b/include/verilated_random.h @@ -31,6 +31,7 @@ #include #include #include +#include #include //============================================================================= @@ -212,7 +213,8 @@ class VlRandomizer VL_NOT_FINAL { m_constraints_line; // fileline content of the constraint for unsat constraints std::vector m_softConstraints; // Soft constraints std::map> m_vars; // Solver-dependent - // variables + std::set m_disabledVars; // Variables with rand_mode off (skip write-back) + // variables ArrayInfoMap m_arr_vars; // Tracks each element in array structures for iteration std::vector m_unique_arrays; std::map m_unique_array_sizes; @@ -337,6 +339,12 @@ public: "supported currently."); } + // Mark a variable as rand_mode-disabled: solver keeps it in m_vars + // (so constraints still reference it) but skips write-back after solving. + void set_var_disabled(const char* name) { m_disabledVars.insert(name); } + // Clear disabled state for a variable + void clear_var_disabled(const char* name) { m_disabledVars.erase(name); } + // --- write_var to register variables --- // Register scalar variable (non-struct, basic type) template diff --git a/src/V3AstAttr.h b/src/V3AstAttr.h index 49a6554f9..cdfb5442f 100644 --- a/src/V3AstAttr.h +++ b/src/V3AstAttr.h @@ -823,6 +823,8 @@ public: RANDOMIZER_SOLVE_BEFORE, RANDOMIZER_PIN_VAR, RANDOMIZER_WRITE_VAR, + RANDOMIZER_SET_VAR_DISABLED, + RANDOMIZER_CLEAR_VAR_DISABLED, RNG_GET_RANDSTATE, RNG_SET_RANDSTATE, SCHED_ANY_TRIGGERED, @@ -962,6 +964,8 @@ inline std::ostream& operator<<(std::ostream& os, const VCMethod& rhs) { {RANDOMIZER_SOLVE_BEFORE, "solveBefore", false}, \ {RANDOMIZER_PIN_VAR, "pin_var", false}, \ {RANDOMIZER_WRITE_VAR, "write_var", false}, \ + {RANDOMIZER_SET_VAR_DISABLED, "set_var_disabled", false}, \ + {RANDOMIZER_CLEAR_VAR_DISABLED, "clear_var_disabled", false}, \ {RNG_GET_RANDSTATE, "__Vm_rng.get_randstate", true}, \ {RNG_SET_RANDSTATE, "__Vm_rng.set_randstate", false}, \ {SCHED_ANY_TRIGGERED, "anyTriggered", false}, \ diff --git a/src/V3LinkDot.cpp b/src/V3LinkDot.cpp index 5091c2240..f9c5abbf2 100644 --- a/src/V3LinkDot.cpp +++ b/src/V3LinkDot.cpp @@ -4999,6 +4999,9 @@ class LinkDotResolveVisitor final : public VNVisitor { nodep->taskp(randFuncp); nodep->classOrPackagep(VN_AS(m_modp, Class)); m_curSymp = m_statep->insertBlock(m_curSymp, nodep->name(), randFuncp, m_modp); + // Already linked to the class randomize function; skip findSymPrefixed + // which may find std::randomize and overwrite classOrPackagep + return; } if (m_insideClassExtParam) { // The reference may point to a method declared in a super class, which is proved diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index 844be5753..65825503c 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -1328,7 +1328,6 @@ class ConstraintExprVisitor final : public VNVisitor { methodp->addPinsp(varnamep); methodp->addPinsp( new AstConst{varp->dtypep()->fileline(), AstConst::Unsized64{}, dimension}); - // Don't pass randMode.index for global constraints with membersel if (randMode.usesMode && !(isGlobalConstrained && membersel)) { methodp->addPinsp( new AstConst{varp->fileline(), AstConst::Unsized64{}, randMode.index}); @@ -1347,7 +1346,52 @@ class ConstraintExprVisitor final : public VNVisitor { UASSERT_OBJ(initTaskp, classp, "No new() in class"); } } + // For globalConstrained sub-object members with rand_mode: + // Always call write_var (keeps variable in solver for constraint + // evaluation), but toggle disabled state so the solver skips + // write-back when rand_mode is off. initTaskp->addStmtsp(methodp->makeStmt()); + if (isGlobalConstrained && membersel && randMode.usesMode) { + AstNodeModule* const varClassp = VN_AS(varp->user2p(), NodeModule); + AstVar* const subRandModeVarp = VN_AS(varClassp->user2p(), Var); + if (subRandModeVarp) { + AstNodeExpr* const parentAccess = membersel->fromp()->cloneTree(false); + AstMemberSel* const randModeSel + = new AstMemberSel{varp->fileline(), parentAccess, subRandModeVarp}; + randModeSel->dtypep(subRandModeVarp->dtypep()); + AstCMethodHard* const atp + = new AstCMethodHard{varp->fileline(), randModeSel, VCMethod::ARRAY_AT, + new AstConst{varp->fileline(), randMode.index}}; + atp->dtypeSetUInt32(); + // rand_mode ON: clear disabled state + AstCMethodHard* const enablep = new AstCMethodHard{ + varp->fileline(), + new AstVarRef{varp->fileline(), VN_AS(m_genp->user2p(), NodeModule), + m_genp, VAccess::READWRITE}, + VCMethod::RANDOMIZER_CLEAR_VAR_DISABLED}; + enablep->dtypeSetVoid(); + AstNodeExpr* const ennp + = new AstCExpr{varp->fileline(), AstCExpr::Pure{}, + "\"" + smtName + "\"", varp->width()}; + ennp->dtypep(varp->dtypep()); + enablep->addPinsp(ennp); + // rand_mode OFF: set disabled state + AstCMethodHard* const disablep = new AstCMethodHard{ + varp->fileline(), + new AstVarRef{varp->fileline(), VN_AS(m_genp->user2p(), NodeModule), + m_genp, VAccess::READWRITE}, + VCMethod::RANDOMIZER_SET_VAR_DISABLED}; + disablep->dtypeSetVoid(); + AstNodeExpr* const disnp + = new AstCExpr{varp->fileline(), AstCExpr::Pure{}, + "\"" + smtName + "\"", varp->width()}; + disnp->dtypep(varp->dtypep()); + disablep->addPinsp(disnp); + AstIf* const ifp = new AstIf{varp->fileline(), atp, enablep->makeStmt(), + disablep->makeStmt()}; + initTaskp->addStmtsp(ifp); + } + } // If randc, also emit markRandc() for cyclic tracking if (varp->isRandC()) { AstCMethodHard* const markp = new AstCMethodHard{ @@ -3087,6 +3131,29 @@ class RandomizeVisitor final : public VNVisitor { } } }); + // Also account for rand_mode indices from globalConstrained sub-objects. + // When constraints from sub-objects are flattened into this class's constraint + // setup functions, the generated code references this->__Vrandmode.at(index). + // We must ensure this class's __Vrandmode is initialized to cover those indices. + { + std::function findSubObjRandModes = [&](AstClass* subClassp) { + subClassp->foreachMember([&](AstClass*, AstNode* subMemberp) { + if (AstVar* const subVarp = VN_CAST(subMemberp, Var)) { + const RandomizeMode rm = {.asInt = subVarp->user1()}; + if (!rm.usesMode) return; + const uint32_t needed = rm.index + 1; + if (needed > randModeCount) randModeCount = needed; + } + }); + }; + classp->foreachMember([&](AstClass*, AstVar* memberVarp) { + if (!memberVarp->globalConstrained()) return; + const AstNodeDType* const dtypep = memberVarp->dtypep()->skipRefp(); + const AstClassRefDType* const classRefp = VN_CAST(dtypep, ClassRefDType); + if (!classRefp) return; + findSubObjRandModes(classRefp->classp()); + }); + } if (hasConstraints) createRandomGenerator(classp); if (randModeCount > 0) { AstVar* const randModeVarp = getCreateRandModeVar(classp); diff --git a/test_regress/t/t_randomize_randmode_subobj.py b/test_regress/t/t_randomize_randmode_subobj.py new file mode 100755 index 000000000..db1adb3f9 --- /dev/null +++ b/test_regress/t/t_randomize_randmode_subobj.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_randomize_randmode_subobj.v b/test_regress/t/t_randomize_randmode_subobj.v new file mode 100644 index 000000000..3f8355e9f --- /dev/null +++ b/test_regress/t/t_randomize_randmode_subobj.v @@ -0,0 +1,90 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 PlanV GmbH +// SPDX-License-Identifier: CC0-1.0 + +// verilog_format: off +`define stop $stop +`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); +`define check_range(gotv,minv,maxv) do if ((gotv) < (minv) || (gotv) > (maxv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d-%0d\n", `__FILE__,`__LINE__, (gotv), (minv), (maxv)); `stop; end while(0); +// verilog_format: on + +// Test: rand_mode(0) on nested sub-object members must prevent solver +// from overwriting the variable. Previously, write_var registered the +// variable unconditionally, so the solver wrote it back even when +// rand_mode was off. + +class InnerClass; + rand bit [7:0] val1; + rand bit [7:0] val2; + rand bit [7:0] val3; + constraint inner_c { + val1 inside {[8'd10 : 8'd50]}; + val2 inside {[8'd60 : 8'd100]}; + val3 inside {[8'd110 : 8'd150]}; + } + function new(); + val1 = 0; + val2 = 0; + val3 = 0; + endfunction +endclass + +class OuterClass; + rand InnerClass nested; + rand bit [7:0] outer_val; + constraint outer_c { outer_val inside {[8'd1 : 8'd20]}; } + function new(); + nested = new(); + outer_val = 0; + endfunction +endclass + +module t; + initial begin + OuterClass obj; + obj = new; + + // Test 1: Normal randomize + repeat (20) begin + `checkd(obj.randomize(), 1) + `check_range(obj.nested.val1, 8'd10, 8'd50) + `check_range(obj.nested.val2, 8'd60, 8'd100) + `check_range(obj.nested.val3, 8'd110, 8'd150) + `check_range(obj.outer_val, 8'd1, 8'd20) + end + + // Test 2: rand_mode(0) on val1 -- must hold assigned value + void'(obj.nested.val1.rand_mode(0)); + obj.nested.val1 = 8'd42; + repeat (20) begin + `checkd(obj.randomize(), 1) + `checkd(obj.nested.val1, 8'd42) + `check_range(obj.nested.val2, 8'd60, 8'd100) + `check_range(obj.nested.val3, 8'd110, 8'd150) + end + + // Test 3: rand_mode(0) on val2 as well + void'(obj.nested.val2.rand_mode(0)); + obj.nested.val2 = 8'd77; + repeat (20) begin + `checkd(obj.randomize(), 1) + `checkd(obj.nested.val1, 8'd42) + `checkd(obj.nested.val2, 8'd77) + `check_range(obj.nested.val3, 8'd110, 8'd150) + end + + // Test 4: Re-enable both and verify randomization resumes + void'(obj.nested.val1.rand_mode(1)); + void'(obj.nested.val2.rand_mode(1)); + repeat (20) begin + `checkd(obj.randomize(), 1) + `check_range(obj.nested.val1, 8'd10, 8'd50) + `check_range(obj.nested.val2, 8'd60, 8'd100) + end + + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule