Part 3 · Constraint Randomization · Intermediate
The randomize() Call
Return-value checking, what gets solved, randomize(null) check-only mode, and field-list arguments.
A built-in virtual method with a contract
Every class implicitly gets virtual function int randomize(); — you never declare it, cannot override it, and call it on an object handle. Its contract is precise: gather every active constraint (class constraints with constraint_mode on, plus any inline with-clause), solve for every rand/randc field whose rand_mode is on, and 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.
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
bus_txn t = new();
initial begin
// THE idiom: assert the return value
assert (t.randomize())
else $fatal(1, "bus_txn randomize failed");
// Equally common in UVM code:
if (!t.randomize())
$error("randomize failed: constraints contradict");
endWhat the solver does on success: it found an (addr, len) pair satisfying all three constraint expressions simultaneously and wrote both fields. On failure it found the constraint set unsatisfiable — and crucially addr and len keep whatever values they had before the call . A test that ignores the return value happily drives that stale data into the DUT, which is why simulators warn on a discarded randomize() return and why coding standards mandate the assert.
Why the return must be checked
UNCHECKED randomize() — the silent-stale-data failure
call 1: t.randomize() → success t = {addr:0x1A40, len:8}
call 2: t.randomize() → success t = {addr:0x1C04, len:3}
(test adds inline constraint that contradicts c_addr)
call 3: t.randomize() with {addr == 0;}
→ FAILS, returns 0
→ t STILL = {addr:0x1C04, len:3}
call 3 sends the txn anyway
│
▼
DUT receives a DUPLICATE of call 2's transaction
• no compile error • no runtime crash • stimulus silently degraded
• coverage counts a transaction that tests nothing newWhat gets randomized — and what does not
The solve set is: every rand / randc field of the object, recursively including the rand fields of any non-null rand class handle it contains. Non-rand fields are state variables: the solver reads them (constraints may reference them) but never writes them. Fields disabled via rand_mode(0) are temporarily treated as state variables too.
class payload;
rand bit [7:0] bytes_q[$];
endclass
class frame;
rand bit [15:0] id; // solved
bit [3:0] rev; // NOT rand → read-only state for solver
rand payload pl; // non-null → solver recurses into bytes_q
payload dbg; // not rand → never touched, even if non-null
constraint c_rev { id[15:12] == rev; } // legal: constrains rand BY state
endclass
frame f = new();
initial begin
f.pl = new(); // MUST construct before randomize
f.rev = 4'h3;
assert (f.randomize());
// solver wrote: f.id (with id[15:12]==3), f.pl.bytes_q
// solver read : f.rev
// untouched : f.dbg (whatever it points to)
endWhat the solver does: it treats rev as a constant 3 during this solve, so id's top nibble is forced to 3; it descends into pl because pl is rand and non-null, randomizing the queue contents and size together with id in one global solve — a constraint in frame could legally reference pl.bytes_q.size(). dbg is invisible to the solver regardless of contents.
randomize(null) — check-only mode
Passing null as the argument list turns the call into a checker: no field is changed, and the return value reports whether the CURRENT values of all rand fields satisfy all active constraints. It is the standard way to validate hand-built or mutated transactions against the class's legality rules.
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 — bad capture or bad rules");
// Also useful after mutating one field of a previously random txn:
t.len = 200; // out of [1:16]
assert (!t.randomize(null)); // correctly reports illegal
endWhat the solver does: it evaluates the constraint set with every rand field pinned to its current value — effectively asking “is this exact point inside the solution space?”. Nothing is written either way. This is also a debug tool: when randomize() fails, pinning fields one at a time with check-only calls helps isolate which value combination is contradictory.
Field-list arguments — partial randomization
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 held at its current value and treated as state. All active constraints still apply, so the solver must find an addr consistent with the frozen len.
bus_txn t = new();
initial begin
assert (t.randomize()); // full solve: addr and len
bit [7:0] keep_len = t.len;
assert (t.randomize(addr)); // re-roll ONLY addr
// len is frozen at keep_len; solver picks a new addr that still
// satisfies c_addr and c_len given that fixed len
assert (t.len == keep_len); // holds: len was not in solve set
// The interaction to remember: a field in the LIST is randomized
// even if it is NOT declared rand. Non-listed rand fields freeze.
endTwo subtle semantics interviewers probe: (1) listed variables are randomized even if not declared rand — the argument list overrides the declaration for this call; (2) this is the clean fix for the randc-slot-burning problem from the rand-vs-randc lesson, since unlisted randc fields do not advance their cycle. Constraints are never relaxed — only the set of free variables changes.
Interview angle
“What does randomize() return and what happens to fields on failure?” — 1/0; on failure ALL fields keep prior values; the solve is atomic.
“How do you check a hand-built transaction against constraints without changing it?” — randomize(null) check-only mode.
“How do you re-randomize one field while keeping the others?” — randomize(field) argument list; others freeze as state variables.
“Can you randomize a non-rand field?” — Yes, by naming it in the argument list for that call.
“Is randomize() virtual? Can you override it?” — It behaves as a built-in virtual method; you cannot override it, you customize via pre/post_randomize and constraints.
Key takeaways
randomize() is atomic: full legal solution committed and return 1, or nothing changed and return 0 — always assert the return.
The solve set = rand/randc fields with rand_mode on, recursing into non-null rand class handles; non-rand fields are read-only state.
randomize(null) checks current values against constraints without modifying anything — validation and debug tool.
randomize(field_list) solves only listed variables (even non-rand ones) and freezes everything else, with all constraints still enforced.
Common pitfalls
Discarding the return value — contradictions silently re-send stale transactions; use assert(obj.randomize()).
Wrapping randomize in assert when assertions are disabled by the tool flow (e.g. +noassert) — the call itself may be skipped; use if(!...) $error in such flows.
Forgetting that randomize(field) freezes OTHER rand fields — distributions across calls change versus a full solve.
Expecting randomize(null) to fix or normalize fields — it is purely a predicate; it never writes.
Assuming non-rand fields can never change under randomize — they can, if explicitly named in the argument list.