Part 3 · Constraint Randomization · Intermediate

Q&A: rand, randc & randomize()

rand vs randc, the randomize() return value, what actually gets randomized, pre/post_randomize uses, and randomizing without a class.

Q: What is the difference between rand and randc?

Direct answer: rand gives independent random values on every solve — repeats can occur on consecutive calls. randc is random-cyclic: the variable steps through a random permutation of its entire value space, never repeating until every value has been used, then reshuffles into a new permutation. The cycling state lives per variable, per object instance , advanced by successive randomize() calls on that instance.

systemverilog
class demo;
  rand  bit [1:0] r;   // any of 0..3 each call; 2,2,2 is possible
  randc bit [1:0] c;   // e.g. 3,0,2,1 then reshuffle: 1,3,0,2 ...
endclass

initial begin
  demo d = new();
  repeat (8) begin
    void'(d.randomize());
    $display("r=%0d c=%0d", d.r, d.c);
  end
end

Follow-up you should expect

“Why not make everything randc?” — Because cycling costs permutation memory (tools cap randc width, typically 8–16 bits), randc cannot take dist weights, randc variables are solved before rand ones (which constrains the solution space ordering), and most stimulus genuinely wants independent draws — exhaustive-without-repeat is the special case, useful for things like sweeping all opcodes or register addresses.

Junior vs senior answer

  • Junior: “randc doesn’t repeat values, rand does.” True but incomplete — misses per-instance scope and the reshuffle-after-exhaustion behavior.

  • Senior: adds that cycling is per-instance state across calls (a new object starts a fresh cycle), that tools cap randc width, and that dist is illegal on randc — pre-empting three follow-ups in one answer.


Q: What does randomize() return, and why must you check it?

Direct answer: it returns 1 if the solver found a solution satisfying all active constraints, 0 if the constraint set was infeasible. On failure the rand variables keep their previous values — so an unchecked failed randomize silently re-sends stale stimulus, which is one of the nastiest bug classes in constrained-random benches: the test passes, coverage looks plausible, and the intended scenario never ran.

systemverilog
if (!pkt.randomize()) begin
  `uvm_fatal("RANDFAIL", "pkt.randomize() failed — constraint conflict")
end

// Anti-pattern that hides failures:
// void'(pkt.randomize());   // failure → stale fields, no signal at all

Follow-up you should expect

“When would it legitimately return 0?” — A contradiction between class constraints and an inline with clause; a subclass constraint contradicting a base constraint; state variables (set by the test) shrinking a range to empty; or arithmetic width wraps making a constraint unsatisfiable. The senior point: failure is a design signal — fatal out immediately rather than tolerating it, because every silent failure poisons all stimulus after it.

Junior vs senior answer

  • Junior: “it returns 1 on success, 0 on failure.” Stops there.

  • Senior: states the stale-values behavior on failure, names the silent-stale-stimulus bug class, and says the bench should treat 0 as fatal — demonstrating debugging scar tissue, not just LRM recall.


Q: When randomize() is called on an object, what exactly gets randomized?

Direct answer: every variable declared rand or randc in that class and its base classes, whose rand_mode is on — including rand fields of nested rand object handles, which are solved together with the parent in one constraint problem . Non-rand (state) variables are never changed; they are read as constants by any constraint that mentions them. Null object handles are not allocated — randomize solves, it never constructs.

systemverilog
class header;
  rand bit [7:0] len;
endclass

class packet;
  rand header     h;          // nested rand object — solved jointly
  rand bit [7:0]  payload_len;
  bit  [7:0]      max_len;    // state: read, never written, by the solver

  constraint c { h.len + payload_len <= max_len; }   // crosses the boundary

  function new();
    h = new();   // MUST exist before randomize() — solver never allocates
  endfunction
endclass

Follow-up you should expect

“What if h were not declared rand?” — Then h.len is treated as state: read by the constraint at its current value, never assigned. The cross-boundary constraint silently becomes a one-sided constraint on payload_len only. That “forgot rand on the handle” mistake is a staple debug question.

Junior vs senior answer

  • Junior: “all the rand variables in the class.”

  • Senior: adds base-class fields, rand_mode gating, joint solving of nested rand objects, the state-variables-as-constants rule, and that null handles are skipped, not allocated.


Q: What are pre_randomize() and post_randomize() for?

Direct answer: they are hooks the simulator calls automatically around the solve — pre_randomize() just before (last chance to set state variables, flip rand_mode/constraint_mode, or compute bounds the constraints will read), post_randomize() just after a successful solve (compute derived fields like checksums/parity, pack bytes, log the solved transaction). Both cascade through nested rand objects, and post_randomize() is skipped when the solve fails.

systemverilog
class frame;
  rand bit [7:0] data[16];
  bit [7:0] crc;          // deliberately NOT rand

  function void post_randomize();
    crc = compute_crc(data);   // pure function of solved fields
  endfunction
endclass

Follow-up you should expect

“Why compute crc in post_randomize() rather than constrain it?” — Because it is fully determined by other fields: constraining it spends solver effort for zero added randomness. The exception that shows judgment: when you need corrupted CRCs at a controlled rate, make crc rand with a dist between correct and corrupt — now it is a randomization decision and belongs in the solver.

Junior vs senior answer

  • Junior: “pre runs before randomize, post runs after.” Restates the names.

  • Senior: gives the canonical uses (state setup vs derived-field computation), notes post is skipped on failure, and articulates the solver-vs-procedural ownership rule with the corrupt-CRC exception.


Q: Can you randomize without a class?

Direct answer: yes — std::randomize() (scope randomization) randomizes local or module-scope variables and accepts inline constraints via with; $urandom / $urandom_range(hi, lo) give quick unconstrained values that are thread-stable for reproducibility (unlike the legacy $random). What you lose without a class: reusable named constraints, constraint layering through inheritance, rand_mode/constraint_mode control, and randc.

systemverilog
int unsigned delay, burst;

initial begin
  if (!std::randomize(delay, burst) with { delay inside {[10:100]};
                                           burst inside {[1:8]};
                                           delay > burst * 10; })
    $fatal(1, "scope randomize failed");

  // Quick one-off:
  delay = $urandom_range(100, 10);
end

Follow-up you should expect

“When do you actually use std::randomize?” — Local decisions that no other component needs to override: a loop count in a sequence body, a one-off delay in a driver. The moment a value needs test-level override or layered constraints, it belongs in a class field. Saying where the line sits is the senior part of the answer.

Junior vs senior answer

  • Junior: “yes, $random.” — names the legacy, non-thread-stable call and stops.

  • Senior: distinguishes std::randomize-with from $urandom_range, mentions thread-stable seeding for reproducibility, and states what class-based randomization adds that scope randomization cannot.

Key takeaways

  • rand = independent draws; randc = per-instance exhaustive cycling across calls.

  • randomize() returning 0 leaves stale values — unchecked calls are silent stimulus corruption.

  • Solver owns rand fields; post_randomize() owns derived fields; state variables are constants.

  • std::randomize covers local decisions; classes add layering, override, and randc.

Common pitfalls

  • Expecting randc behavior across different object instances — each instance cycles independently.

  • void'(randomize()) in production sequences — infeasibility becomes invisible.

  • Forgetting rand on a nested handle — its constraints silently degrade to one-sided.

  • Computing derived fields in post_randomize() and then expecting inline constraints on them to work.