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 in the testbench below); 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 in the testbench below); 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.
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, then one contradiction that must leave the object untouched.
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;
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");
$display("PASS: 100 legal solves, and a failed solve left the object untouched");
$finish;
end
endmodule%Warning-UNSATCONSTR: /var/folders/41/dgs80y1569b4x8pwflv_w9jr0000gn/T/rtbtb-ZYw5Pz/t115.sv:4: Unsatisfied constraint: 'constraint c_addr { addr inside {[32'h0000_1000 : 32'h0000_1FFF]}; }'
%Warning-UNSATCONSTR: /var/folders/41/dgs80y1569b4x8pwflv_w9jr0000gn/T/rtbtb-ZYw5Pz/t115.sv:23: Unsatisfied constraint: 'ok = t.randomize() with { addr == 0; };'
contradiction: randomize returned 0, addr 0000142c -> 0000142c, len 9 -> 9
PASS: 100 legal solves, and a failed solve left the object untouchedYes. 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.
Run 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.