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.

systemverilog
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");
end

What 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

diagram
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 new

What 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.

systemverilog
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)
end

What 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.

systemverilog
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
end

What 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.

systemverilog
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.
end

Two 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.