diff --git a/src/V3Randomize.cpp b/src/V3Randomize.cpp index 5fe7432f8..9d477065b 100644 --- a/src/V3Randomize.cpp +++ b/src/V3Randomize.cpp @@ -1176,6 +1176,17 @@ class ConstraintExprVisitor final : public VNVisitor { nodep->user1(anyChild); } + // Prepend the bucket preamble stmts stored on user3p by lowerDistConstraints to the + // statements that will become the body of the generated AstForeach, so the per-element + // bucket draw is declared and evaluated in the same scope as its references. + static AstNode* prependDistPreamble(AstConstraintForeach* nodep, AstNode* bodyp) { + AstNode* const preamblep = nodep->user3p(); + if (!preamblep) return bodyp; + nodep->user3p(nullptr); + preamblep->addNext(bodyp); + return preamblep; + } + // VISITORS void visit(AstNodeVarRef* nodep) override { AstVar* varp = nodep->varp(); @@ -2306,25 +2317,22 @@ class ConstraintExprVisitor final : public VNVisitor { cstmtp->add("ret += "); cstmtp->add(itemp); cstmtp->add(";"); + AstNode* const bodyp = prependDistPreamble(nodep, cstmtp); AstCExpr* const cexprp = new AstCExpr{fl}; cexprp->dtypeSetString(); cexprp->add("([&]{\nstd::string ret;\n"); cexprp->add(new AstBegin{ - fl, "", new AstForeach{fl, nodep->headerp()->unlinkFrBack(), cstmtp}, true}); + fl, "", new AstForeach{fl, nodep->headerp()->unlinkFrBack(), bodyp}, true}); cexprp->add("return ret.empty() ? \"#b1\" : \"(bvand\" + ret + \")\";\n})()"); nodep->replaceWith(new AstSFormatF{fl, "%s", false, cexprp}); } else { iterateAndNextNull(nodep->bodyp()); - AstNode* bodyp = nodep->bodyp()->unlinkFrBackWithNext(); - // Prepend bucket preamble stmts stored by lowerDistConstraints (foreach case) - if (AstNode* const preamblep = nodep->user3p()) { - preamblep->addNext(bodyp); - bodyp = preamblep; - nodep->user3p(nullptr); - } + AstNode* const bodyp + = prependDistPreamble(nodep, nodep->bodyp()->unlinkFrBackWithNext()); nodep->replaceWith(new AstBegin{ fl, "", new AstForeach{fl, nodep->headerp()->unlinkFrBack(), bodyp}, true}); } + UASSERT_OBJ(!nodep->user3p(), nodep, "Dist bucket preamble not injected into foreach"); VL_DO_DANGLING(nodep->deleteTree(), nodep); } void visit(AstConstraintBefore* nodep) override { diff --git a/test_regress/t/t_constraint_dist_if_foreach.py b/test_regress/t/t_constraint_dist_if_foreach.py new file mode 100755 index 000000000..db1adb3f9 --- /dev/null +++ b/test_regress/t/t_constraint_dist_if_foreach.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_if_foreach.v b/test_regress/t/t_constraint_dist_if_foreach.v new file mode 100644 index 000000000..6f00745b8 --- /dev/null +++ b/test_regress/t/t_constraint_dist_if_foreach.v @@ -0,0 +1,493 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 BRDR LIFE +// SPDX-License-Identifier: CC0-1.0 + +// Test that a dist constraint inside a foreach nested within a constraint if +// produces values only within the declared distribution, covers all buckets, +// honors the declared weights, and draws a bucket per array element. + +// 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 checkgt(gotv,minv) do if (!((gotv) > (minv))) begin \ + $write("%%Error: %s:%0d: got=%0d expected > %0d\n", `__FILE__, `__LINE__, (gotv), (minv)); `stop; end while(0) +// verilog_format: on + +// if (gate) foreach (a[i]) a[i] dist {...} +class ClsIf; + rand bit [3:0] a[4]; + bit gate; + constraint c { + if (gate == 1'b1) { + foreach (a[i]) { + a[i] dist { + 4'd0 := 3, + [4'd1 : 4'd4] := 1 + }; + } + } + } +endclass + +// if (gate) foreach (a[i]) a[i] dist {...} else foreach (a[i]) a[i] dist {...} +class ClsIfElse; + rand bit [3:0] a[4]; + bit gate; + constraint c { + if (gate == 1'b1) { + foreach (a[i]) { + a[i] dist { + 4'd0 := 3, + [4'd1 : 4'd4] := 1 + }; + } + } else { + foreach (a[i]) {a[i] dist {[4'd8 : 4'd11] := 1};} + } + } +endclass + +// if (gate) foreach (a[i]) gate2 -> a[i] dist {...} +class ClsIfImpl; + rand bit [3:0] a[4]; + bit gate, gate2; + constraint c { + if (gate == 1'b1) { + foreach (a[i]) { + gate2 -> + (a[i] dist { + 4'd0 := 3, + [4'd1 : 4'd4] := 1 + }); + } + } + } +endclass + +// if (sel) foreach (a[i]) a[i] dist {...}, with a randomized condition +class ClsRandSel; + rand bit [2:0] a[4]; + rand bit sel; + constraint c { + if (sel) { + foreach (a[i]) { + a[i] dist { + [3'd0 : 3'd1] :/ 90, + [3'd2 : 3'd5] :/ 10 + }; + } + } + } +endclass + +// Two dist constraints sharing one foreach body inside a constraint if +class ClsMulti; + rand bit [2:0] a[4]; + rand bit [2:0] b[4]; + bit gate; + constraint c { + if (gate == 1'b1) { + foreach (a[i]) { + a[i] dist { + [3'd0 : 3'd1] :/ 90, + [3'd2 : 3'd5] :/ 10 + }; + b[i] dist {3'd7 := 1}; + } + } + } +endclass + +// if (gate) foreach (m[i]) foreach (m[i][j]) m[i][j] dist {...} +class ClsNested; + rand bit [2:0] m[2][2]; + bit gate; + constraint c { + if (gate == 1'b1) { + foreach (m[i]) { + foreach (m[i][j]) { + m[i][j] dist { + [3'd0 : 3'd1] :/ 90, + [3'd2 : 3'd5] :/ 10 + }; + } + } + } + } +endclass + +// Two equally weighted, non-adjacent buckets, so a per-element bucket draw is +// observable as differing elements within a single randomize() call +class ClsIndep; + rand bit [3:0] a[8]; + bit gate; + constraint c { + if (gate == 1'b1) { + foreach (a[i]) { + a[i] dist { + 4'd0 := 1, + 4'd9 := 1 + }; + } + } + } +endclass + +// if (gate) foreach (q[i]) q[i] dist {...}, over a queue +class ClsQueue; + rand bit [2:0] q[$]; + bit gate; + constraint sz {q.size() == 4;} + constraint c { + if (gate == 1'b1) { + foreach (q[i]) { + q[i] dist { + [3'd0 : 3'd1] :/ 90, + [3'd2 : 3'd5] :/ 10 + }; + } + } + } +endclass + +// if (gate) foreach (d[i]) d[i] dist {...}, over a dynamic array +class ClsDynArray; + rand bit [2:0] d[]; + bit gate; + constraint sz {d.size() == 4;} + constraint c { + if (gate == 1'b1) { + foreach (d[i]) { + d[i] dist { + [3'd0 : 3'd1] :/ 90, + [3'd2 : 3'd5] :/ 10 + }; + } + } + } +endclass + +// Weight given by a variable rather than a literal +class ClsVarWeight; + rand bit [2:0] a[4]; + bit gate; + int w; + constraint c { + if (gate == 1'b1) { + foreach (a[i]) { + a[i] dist { + [3'd0 : 3'd1] :/ w, + [3'd2 : 3'd5] :/ 1 + }; + } + } + } +endclass + +// foreach (m[i]) if (gate) foreach (m[i][j]) m[i][j] dist {...}, so the +// constraint if sits between the two foreach levels +class ClsForeachIfForeach; + rand bit [2:0] m[2][4]; + bit gate; + constraint c { + foreach (m[i]) { + if (gate == 1'b1) { + foreach (m[i][j]) { + m[i][j] dist { + [3'd0 : 3'd1] :/ 90, + [3'd2 : 3'd5] :/ 10 + }; + } + } + } + } +endclass + +// if (gate) foreach (a[i]) if (gate2) a[i] dist {...} +class ClsIfForeachIf; + rand bit [2:0] a[4]; + bit gate, gate2; + constraint c { + if (gate == 1'b1) { + foreach (a[i]) { + if (gate2 == 1'b1) { + a[i] dist { + [3'd0 : 3'd1] :/ 90, + [3'd2 : 3'd5] :/ 10 + }; + } + } + } + } +endclass + +module t; + initial begin + // dist inside if + foreach stays in range and reaches both buckets + begin + static ClsIf obj = new(); + int seen_zero, seen_nonzero; + obj.gate = 1'b1; + seen_zero = 0; + seen_nonzero = 0; + repeat (100) begin + `checkd(obj.randomize(), 1); + foreach (obj.a[i]) begin + `checkd((obj.a[i] inside {[4'd0 : 4'd4]}), 1'b1); + if (obj.a[i] == 0) seen_zero++; + else seen_nonzero++; + end + end + `checkgt(seen_zero, 0); + `checkgt(seen_nonzero, 0); + end + + // Each arm of an if/else selects its own distribution + begin + static ClsIfElse obj = new(); + int seen_zero, seen_nonzero, seen_else; + obj.gate = 1'b1; + seen_zero = 0; + seen_nonzero = 0; + repeat (100) begin + `checkd(obj.randomize(), 1); + foreach (obj.a[i]) begin + `checkd((obj.a[i] inside {[4'd0 : 4'd4]}), 1'b1); + if (obj.a[i] == 0) seen_zero++; + else seen_nonzero++; + end + end + `checkgt(seen_zero, 0); + `checkgt(seen_nonzero, 0); + obj.gate = 1'b0; + seen_else = 0; + repeat (100) begin + `checkd(obj.randomize(), 1); + foreach (obj.a[i]) begin + `checkd((obj.a[i] inside {[4'd8 : 4'd11]}), 1'b1); + seen_else++; + end + end + `checkd(seen_else, 400); + end + + // dist under an implication nested inside the if + foreach + begin + static ClsIfImpl obj = new(); + int seen_zero, seen_nonzero; + obj.gate = 1'b1; + obj.gate2 = 1'b1; + seen_zero = 0; + seen_nonzero = 0; + repeat (100) begin + `checkd(obj.randomize(), 1); + foreach (obj.a[i]) begin + `checkd((obj.a[i] inside {[4'd0 : 4'd4]}), 1'b1); + if (obj.a[i] == 0) seen_zero++; + else seen_nonzero++; + end + end + `checkgt(seen_zero, 0); + `checkgt(seen_nonzero, 0); + end + + // A rand if condition applies the dist to every element when taken, and the + // 90 :/ 10 weights make the low bucket dominate + begin + static ClsRandSel obj = new(); + int seen_low, seen_high; + seen_low = 0; + seen_high = 0; + repeat (100) begin + `checkd(obj.randomize() with {sel == 1'b1;}, 1); + `checkd(obj.sel, 1'b1); + foreach (obj.a[i]) begin + `checkd((obj.a[i] inside {[3'd0 : 3'd5]}), 1'b1); + if (obj.a[i] <= 1) seen_low++; + else seen_high++; + end + end + `checkgt(seen_low, 0); + `checkgt(seen_high, 0); + `checkgt(seen_low, seen_high); + end + + // Two dist constraints in one foreach body are both honored + begin + static ClsMulti obj = new(); + int seen_low, seen_high; + obj.gate = 1'b1; + seen_low = 0; + seen_high = 0; + repeat (100) begin + `checkd(obj.randomize(), 1); + foreach (obj.a[i]) begin + `checkd((obj.a[i] inside {[3'd0 : 3'd5]}), 1'b1); + `checkd(obj.b[i], 3'd7); + if (obj.a[i] <= 1) seen_low++; + else seen_high++; + end + end + `checkgt(seen_low, 0); + `checkgt(seen_high, 0); + `checkgt(seen_low, seen_high); + end + + // A dist in a nested foreach under a constraint if + begin + static ClsNested obj = new(); + int seen_low, seen_high; + obj.gate = 1'b1; + seen_low = 0; + seen_high = 0; + repeat (100) begin + `checkd(obj.randomize(), 1); + foreach (obj.m[i]) begin + foreach (obj.m[i][j]) begin + `checkd((obj.m[i][j] inside {[3'd0 : 3'd5]}), 1'b1); + if (obj.m[i][j] <= 1) seen_low++; + else seen_high++; + end + end + end + `checkgt(seen_low, 0); + `checkgt(seen_high, 0); + `checkgt(seen_low, seen_high); + end + + // The bucket is drawn per element, so one randomize() call can place + // different elements in different buckets + begin + static ClsIndep obj = new(); + int seen_mixed; + obj.gate = 1'b1; + seen_mixed = 0; + repeat (100) begin + int seen_lo, seen_hi; + `checkd(obj.randomize(), 1); + seen_lo = 0; + seen_hi = 0; + foreach (obj.a[i]) begin + `checkd((obj.a[i] inside {4'd0, 4'd9}), 1'b1); + if (obj.a[i] == 4'd0) seen_lo++; + else seen_hi++; + end + if (seen_lo > 0 && seen_hi > 0) seen_mixed++; + end + `checkgt(seen_mixed, 0); + end + + // dist applies to every element of a queue + begin + static ClsQueue obj = new(); + int seen_low, seen_high; + obj.gate = 1'b1; + seen_low = 0; + seen_high = 0; + repeat (100) begin + `checkd(obj.randomize(), 1); + `checkd(obj.q.size(), 4); + foreach (obj.q[i]) begin + `checkd((obj.q[i] inside {[3'd0 : 3'd5]}), 1'b1); + if (obj.q[i] <= 1) seen_low++; + else seen_high++; + end + end + `checkgt(seen_low, 0); + `checkgt(seen_high, 0); + `checkgt(seen_low, seen_high); + end + + // dist applies to every element of a dynamic array + begin + static ClsDynArray obj = new(); + int seen_low, seen_high; + obj.gate = 1'b1; + seen_low = 0; + seen_high = 0; + repeat (100) begin + `checkd(obj.randomize(), 1); + `checkd(obj.d.size(), 4); + foreach (obj.d[i]) begin + `checkd((obj.d[i] inside {[3'd0 : 3'd5]}), 1'b1); + if (obj.d[i] <= 1) seen_low++; + else seen_high++; + end + end + `checkgt(seen_low, 0); + `checkgt(seen_high, 0); + `checkgt(seen_low, seen_high); + end + + // A weight held in a variable is read at run time, so a 3 :/ 1 split + // reaches both buckets and leaves the low one dominant + begin + static ClsVarWeight obj = new(); + int seen_low, seen_high; + obj.gate = 1'b1; + obj.w = 3; + seen_low = 0; + seen_high = 0; + repeat (100) begin + `checkd(obj.randomize(), 1); + foreach (obj.a[i]) begin + `checkd((obj.a[i] inside {[3'd0 : 3'd5]}), 1'b1); + if (obj.a[i] <= 1) seen_low++; + else seen_high++; + end + end + `checkgt(seen_low, 0); + `checkgt(seen_high, 0); + `checkgt(seen_low, seen_high); + end + + // A constraint if between two foreach levels + begin + static ClsForeachIfForeach obj = new(); + int seen_low, seen_high; + obj.gate = 1'b1; + seen_low = 0; + seen_high = 0; + repeat (100) begin + `checkd(obj.randomize(), 1); + foreach (obj.m[i]) begin + foreach (obj.m[i][j]) begin + `checkd((obj.m[i][j] inside {[3'd0 : 3'd5]}), 1'b1); + if (obj.m[i][j] <= 1) seen_low++; + else seen_high++; + end + end + end + `checkgt(seen_low, 0); + `checkgt(seen_high, 0); + `checkgt(seen_low, seen_high); + end + + // A second constraint if inside the foreach body + begin + static ClsIfForeachIf obj = new(); + int seen_low, seen_high; + obj.gate = 1'b1; + obj.gate2 = 1'b1; + seen_low = 0; + seen_high = 0; + repeat (100) begin + `checkd(obj.randomize(), 1); + foreach (obj.a[i]) begin + `checkd((obj.a[i] inside {[3'd0 : 3'd5]}), 1'b1); + if (obj.a[i] <= 1) seen_low++; + else seen_high++; + end + end + `checkgt(seen_low, 0); + `checkgt(seen_high, 0); + `checkgt(seen_low, seen_high); + end + + $write("*-* All Finished *-*\n"); + $finish; + end +endmodule