Every class implicitly gets a built-in randomize method. Its contract is precise: gather every active constraint, solve for every enabled rand field, then either commit a complete legal solution and return 1, or commit nothing and return 0.
There is no partial success. The solve is atomic across all fields, which is why the return value is load-bearing.
// snippet: the idiom, for any object t
if (!t.randomize())
$fatal(1, "randomize failed: constraints contradict");On success the solver found an addr and len pair satisfying all three constraint expressions at once. On failure the constraint set was unsatisfiable, and addr and len keep whatever values they held before the call.
Verilator, for one, prints a WIDTHTRUNC warning for !t.randomize(), because randomize() returns an int; the warning is harmless.
Some lint flows flag a discarded randomize return. The rule does not need the warning: check the return on every call regardless.
Call | Result | What t holds |
|---|---|---|
1. | Returns 1 | A new legal packet |
2. | Returns 1 | Another new legal packet |
3. | Returns 0, unchecked | Still packet 2, so the test sends packet 2 again |
No error, no crash: the design simply receives a duplicate, and coverage counts a transaction that tested nothing new.
The solve set is every rand or randc field with rand mode on, recursing into any non-null rand class handle. Non-rand fields are state variables: the solver reads them but never writes them.
// not-run: Verilator 5.048 cannot constrain size() of a queue inside a nested rand object (it treats it as state), so randomize() fails
class payload;
rand bit [7:0] bytes_q[$];
constraint c_sz { bytes_q.size() inside {[1:16]}; } // without it the queue is not resized
endclass
class frame;
rand bit [15:0] id; // solved
bit [3:0] rev; // NOT rand: read-only state for the solver
rand payload pl; // non-null: the solver recurses into bytes_q
payload dbg; // not rand: never touched, even if non-null
constraint c_rev { id[15:12] == rev; } // constraining rand BY state
endclass
module top;
frame f = new();
initial begin
f.pl = new(); // construct before randomize
f.rev = 4'h3;
assert (f.randomize());
// solver wrote: f.id with id[15:12]==3, plus f.pl.bytes_q
// solver read : f.rev
// untouched : f.dbg
end
endmoduleWith rev held at 3, the top nibble of id is forced to 3. The solver descends into pl because pl is rand and non-null, sizing and filling the queue in the same global solve. Without c_sz the queue would not be resized: IEEE 1800 18.4 says an array whose size is not constrained keeps its size, so an empty queue stays empty.
dbg stays invisible to the solver regardless of its contents.
Passing null as the argument list turns the call into a checker. No field changes, and the return value reports whether the current values satisfy all active constraints.
// not-run: fragment (bus_txn is the class above); Verilator 5.048 also rejects randomize(null)
module top;
bus_txn t = new();
initial begin
// Hand-build a transaction, e.g. replaying a captured trace
t.addr = 32'h0000_1404;
t.len = 4;
if (t.randomize(null))
$display("trace txn is legal per class constraints");
else
$error("trace txn VIOLATES constraints");
// Also useful after mutating one field of a random txn:
t.len = 200; // outside [1:16]
assert (!t.randomize(null)); // correctly reports illegal
end
endmoduleThe solver pins every rand field to its current value and asks whether that exact point lies inside the solution space. Nothing is written either way, which also makes it a debug tool for isolating contradictory combinations.
Calling t.randomize(addr) restricts the solve set to the listed variables. Only addr is treated as random; every other field, including other rand fields, is frozen at its current value as state.
// not-run: fragment (bus_txn is the class above); Verilator 5.048 also re-randomizes len in t.randomize(addr), so the last assert fails
module top;
bus_txn t = new();
bit [7:0] keep_len;
initial begin
assert (t.randomize()); // full solve: addr and len
keep_len = t.len;
assert (t.randomize(addr)); // re-roll ONLY addr
// len is frozen at keep_len; the solver picks a new addr that still
// satisfies c_addr and c_len with that fixed len
assert (t.len == keep_len); // holds: len was not in the solve set
end
endmoduleNot every simulator follows this yet: one of the simulators used here also re-randomized len in t.randomize(addr). Where a field must stay put in this course's runnable listings, it is frozen with rand_mode(0) instead, which the dynamic-control topic teaches.
Two semantics matter here. A listed variable is randomized even if it is not declared rand, and this is the clean fix for randc slot burning.
Constraints are never relaxed; only the set of free variables changes.
Class randomization suits transactions: reusable objects with intrinsic legality rules. Scope randomization suits local, one-off decisions where a class would be ceremony. Plain draws suit single bounded values.
// not-run: fragment (bus_txn is the class above; clk and drive() are not defined here)
// Typical test-level mix of all three tools:
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: plain draw
@(posedge clk);
end
endtaskEach call sits at the right weight class. The std randomize jointly picks count and mode under the implication, while the class call runs the transaction rules.
$urandom_range draws a plain gap with no solver involved.
// not-run: fragment (clk and send_burst() are not defined here)
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);
endtaskThe with-clause is solved jointly: the solver picks a legal pair, not gap then burst. Variables from the surrounding scope that appear in the clause but not in the argument list are state variables, read and never written.
$urandom returns a 32-bit unsigned value and $urandom_range bounds it inclusively, tolerating swapped arguments. They are thread-stable system functions, not solver calls: no constraints, no failure mode, far cheaper than a solve.
// not-run: illustration with no self-check; Icarus also rejects a $urandom seed that is not a variable
module top;
bit [31:0] raw;
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: arguments may be swapped
raw = $urandom(42); // seed is an input expression: reseeds this
// thread's RNG (unlike $random's inout seed)
// 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
end
endmodule$urandom_range | std::randomize | class randomize | |
|---|---|---|---|
Constraints | none | with-clause | class plus inline with |
Cross-variable | no | yes | yes |
Can fail | no | yes, returns 0 | yes, returns 0 |
Cost | trivial | solver call | solver call |
Random stream | calling thread | calling thread | the object |
Rule reuse | none | none, inline | high, inside the class |
Typical use | gaps, delays | local joint picks | transactions |
t.randomize() with { addr == 0; }, which contradicts c_addr. What does the call return, and what happens to addr and len?It returns 0, and both fields keep the values from the previous successful call. A failed solve commits nothing.
A hundred checked solves, one contradiction, a joint std::randomize with an implication, and $urandom_range bounds.
class bus_txn;
rand bit [31:0] addr;
rand bit [7:0] len;
constraint c_addr { addr inside {[32'h0000_1000 : 32'h0000_1FFF]}; }
constraint c_len { len inside {[1:16]}; addr[1:0] == 0; }
endclass
module tb_contract;
bus_txn t = new();
bit [31:0] keep_addr;
bit [7:0] keep_len;
int ok, n_txns, mode, gap;
initial begin
// 1. The idiom: check every call. A success commits a legal pair.
repeat (100) begin
if (!t.randomize()) $fatal(1, "FAIL: randomize failed");
if (t.addr < 32'h1000 || t.addr > 32'h1FFF || t.addr[1:0] != 0 || t.len < 1 || t.len > 16)
$fatal(1, "FAIL: illegal txn addr=%h len=%0d", t.addr, t.len);
end
// 2. A contradiction returns 0 and commits nothing.
keep_addr = t.addr;
keep_len = t.len;
ok = t.randomize() with { addr == 0; };
$display("contradiction: randomize returned %0d, addr %h -> %h, len %0d -> %0d", ok, keep_addr, t.addr, keep_len, t.len);
if (ok != 0 || t.addr != keep_addr || t.len != keep_len) $fatal(1, "FAIL: a failed solve changed the object");
// 3. std::randomize solves local variables jointly.
repeat (100) begin
if (!std::randomize(n_txns, mode) with { n_txns inside {[10:50]}; mode inside {[0:2]}; (mode == 2) -> n_txns > 30; })
$fatal(1, "FAIL: std::randomize failed");
if (mode == 2 && n_txns <= 30) $fatal(1, "FAIL: stress mode with only %0d txns", n_txns);
end
// 4. $urandom_range: bounds inclusive, arguments in either order.
repeat (100) begin
gap = $urandom_range(5, 0);
if (gap < 0 || gap > 5) $fatal(1, "FAIL: gap %0d", gap);
end
$display("PASS: 100 legal solves, a failed solve left the object untouched, std::randomize and $urandom_range held their rules");
$finish;
end
endmodule%Warning-UNSATCONSTR: /var/folders/41/dgs80y1569b4x8pwflv_w9jr0000gn/T/rtbtb-K1WcxI/t3.sv:4: Unsatisfied constraint: 'constraint c_addr { addr inside {[32'h0000_1000 : 32'h0000_1FFF]}; }'
%Warning-UNSATCONSTR: /var/folders/41/dgs80y1569b4x8pwflv_w9jr0000gn/T/rtbtb-K1WcxI/t3.sv:23: Unsatisfied constraint: 'ok = t.randomize() with { addr == 0; };'
contradiction: randomize returned 0, addr 00001f40 -> 00001f40, len 15 -> 15
PASS: 100 legal solves, a failed solve left the object untouched, std::randomize and $urandom_range held their rulesYes. The argument list overrides the declaration for that call: listed variables are randomized, and every other rand field freezes as state.
Elision can remove the assert together with its call, so randomize never runs. The if (!t.randomize()) $error form keeps the call on every flow.
Independent draws cannot express a relationship between variables. One std::randomize with a plus-equals constraint solves the pair jointly.
One way to write it:
typedef enum bit [1:0] {DATA = 2'd0, CTRL = 2'd1, ERR = 2'd2} kind_e;
// PacketFIFO transaction with rand fields: the solver now builds legal packets.
class pkt_txn;
rand kind_e kind;
rand bit [1:0] dest;
rand bit [3:0] len;
rand bit [7:0] payload[8]; // fixed size; only the first len bytes are sent
constraint c_len { len inside {[1:8]}; }
function bit [7:0] header();
return {kind, dest, len};
endfunction
function bit [7:0] check_byte();
bit [7:0] c = header();
for (int i = 0; i < len; i++) c ^= payload[i];
return c;
endfunction
endclass
module tb_pkt_rand;
pkt_txn p = new();
int lens[int];
int kinds[int];
initial begin
repeat (500) begin
if (!p.randomize()) $fatal(1, "FAIL: randomize failed"); // every call checked
if (p.len < 1 || p.len > 8) $fatal(1, "FAIL: illegal len %0d", p.len);
lens[int'(p.len)]++;
kinds[int'(p.kind)]++;
end
$display("lengths seen: %0d of 8, kinds seen: %0d of 3", lens.num(), kinds.num());
if (lens.num() != 8 || kinds.num() != 3) $fatal(1, "FAIL: some legal value never appeared");
$display("PASS: 500 checked solves gave legal packets covering every length and kind");
$finish;
end
endmodulelengths seen: 8 of 8, kinds seen: 3 of 3
PASS: 500 checked solves gave legal packets covering every length and kindRun with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown. Verilator prints a harmless WIDTHTRUNC warning for if (!obj.randomize()), because randomize() returns an int, and an UNSATCONSTR warning when a solve fails on purpose. Icarus 13 does not support constraints.
Not run: randomize(null) is rejected by Verilator 5.048 ("Unsupported"), and its randomize(addr) re-randomized len as well, so the field-list listing is shown but not run.