Part 3 · Constraint Randomization · Intermediate
std::randomize() & Scope Randomization
Randomizing local variables, with-clauses on std::randomize, when scope randomization beats class randomization, and $urandom comparison.
Randomization without a class
std::randomize(vars...) invokes the same constraint solver as class randomize(), but on local or module-scope variables instead of class fields. The listed variables play the role of rand fields; an optional with { ... } clause supplies the constraints. The return-value contract is identical: 1 with all variables updated to a simultaneous legal solution, or 0 with nothing changed.
task drive_idle_gap();
int unsigned gap;
bit [3:0] burst;
// Solve gap and burst TOGETHER under shared constraints
if (!std::randomize(gap, burst) with {
gap inside {[1:100]};
burst inside {[1:8]};
gap > burst * 10; // relationship between the two
})
$error("std::randomize failed");
repeat (gap) @(posedge clk);
send_burst(burst);
endtaskWhat the solver does: exactly the class-randomize pipeline — intersect the three with-clause expressions into a (gap, burst) solution space and pick uniformly. Because gap > burst*10 ties them, the solve is joint: the solver does not pick gap then burst; it picks a legal PAIR. This is the key advantage over calling $urandom_range twice, which cannot express cross-variable relationships.
Scope variables in the with-clause
task drive_after(int unsigned min_gap); // task arg = state variable
int unsigned gap;
// with-clause sees surrounding scope: min_gap is read as a constant
assert (std::randomize(gap) with { gap inside {[min_gap : min_gap+50]}; });
endtaskVariables not listed in the argument list but referenced in the with-clause are state variables — read, never written — mirroring how class constraints read non-rand fields.
When scope randomize beats class randomize
Class randomization is the right tool for transactions — reusable objects with intrinsic legality rules that travel with the data. Scope randomization wins when the random decision is local and ephemeral: delays, loop counts, one-off mode picks inside a test or task, where defining a class would be ceremony with no reuse.
WHICH RANDOMIZATION TOOL?
need cross-variable constraints?
│
no │ yes
│ └────────────────────────┐
▼ ▼
single bounded value? values belong to a reusable
│ transaction with intrinsic rules?
yes │ no │
▼ no │ yes
$urandom_range(hi,lo) │ └──► CLASS randomize()
(fast, no solver, ▼ rand fields + constraints
always succeeds) std::randomize(...) with {...}
(solver power, no class ceremony)
rule of thumb:
• transaction content → class randomize
• local sequencing decisions → std::randomize with
• simple independent draws → $urandom / $urandom_range// Typical test-level mix of all three:
task run_traffic(bus_txn t);
int n_txns, mode;
// joint local decision → std::randomize
assert (std::randomize(n_txns, mode) with {
n_txns inside {[10:50]};
mode inside {[0:2]};
(mode == 2) -> n_txns > 30; // stress mode needs volume
});
repeat (n_txns) begin
assert (t.randomize()); // transaction → class solve
drive(t);
repeat ($urandom_range(0, 5)) // simple gap → $urandom_range
@(posedge clk);
end
endtaskWhat each call does: the std::randomize jointly picks count and mode honoring the implication; t.randomize() runs the transaction's own constraint set; $urandom_range does a plain bounded draw with no solver involvement at all — three tools, each at the right weight class.
$urandom and $urandom_range vs randomize
$urandom returns a 32-bit unsigned random value; $urandom_range(maxval, minval) bounds it inclusively (and tolerates swapped arguments). They are thread-stable system functions, not solver calls: no constraints, no return-value protocol, no failure mode. Two properties matter for choosing them: they are dramatically cheaper than a solver invocation, and they draw from the calling THREAD's random stream (random stability — covered in the seeding lesson), whereas class randomize() draws from the OBJECT's stream.
bit [31:0] raw, lo32;
int unsigned d6;
initial begin
raw = $urandom; // full 32-bit draw
d6 = $urandom_range(6, 1); // inclusive [1:6]
d6 = $urandom_range(1, 6); // same — args may be swapped
raw = $urandom(42); // optional seed arg: seeds THIS
// process's stream, then draws
// What $urandom CANNOT do:
// pick (a,b) with a+b==100 → needs std::randomize with
// honor class legality rules → needs class randomize
// cyclic no-repeat values → needs randc
endComparison table
$urandom_range std::randomize class randomize
constraints none with-clause class + inline with
cross-variable no yes yes
failure possible no yes (returns 0) yes (returns 0)
cost trivial solver call solver call
random stream calling thread calling thread the object
reuse of rules none none (inline) high (in the class)
typical use gaps, delays local joint picks transactionsInterview angle
This topic shows up as a judgment question — “which randomization mechanism would you use for X, and why?” — and as a semantics check on the differences.
“Difference between $urandom and $random?” — $urandom is unsigned, thread-stable, SystemVerilog; $random is the old signed Verilog function with weaker stability guarantees. Use $urandom.
“Can std::randomize fail?” — yes, contradictory with-clause returns 0 and changes nothing; check it exactly like class randomize.
“How do you randomize two locals so their sum is fixed?” — std::randomize(a,b) with { a+b == N; } — $urandom cannot express the relationship.
“Why prefer class constraints over a with-clause on std::randomize for a packet?” — legality rules belong WITH the data type: reusable, overridable by inheritance, checkable via randomize(null).
“Does $urandom_range(1,6) work, or must max come first?” — both orders work; the function takes max,min but swaps if needed.
Key takeaways
std::randomize() is the full constraint solver applied to scope variables; the with-clause holds the constraints and the return value must be checked.
Use class randomize for transactions, std::randomize for local joint decisions, $urandom_range for simple independent draws.
Unlisted variables referenced in a with-clause are state variables — read, never written, like non-rand class fields.
$urandom/$urandom_range never fail and never solve constraints — cheap thread-stable draws, wrong tool for related variables.
Common pitfalls
Ignoring the std::randomize return value — same silent-stale-data failure as class randomize.
Building cross-variable relationships out of sequential $urandom_range calls and rejection loops — slow and distribution-skewed; one std::randomize does it right.
Putting transaction legality rules into scattered with-clauses instead of the class — rules drift apart across tests.
Using $random in new code — signed result and weaker stability cause subtle reproducibility differences across tools.
Assuming $urandom participates in constraint solving — it is a plain draw; constraints never see it.