Part 3 · Constraint Randomization · Intermediate

pre_randomize() & post_randomize()

Automatic hooks around the solve, call order through inheritance and nesting, deriving non-rand fields, and recursion gotchas.

Hooks around the solve

Every class may define function void pre_randomize(); and function void post_randomize();. You never call them directly — randomize() invokes them automatically: pre before the solver runs, post after the solver succeeds. Their division of labor follows from the solver contract: pre is where you set up state the solver will READ (weights, mode flags, rand_mode/constraint_mode switches), post is where you compute values the solver cannot produce (CRCs, parity, timestamps, derived non-rand fields).

systemverilog
class eth_frame;
  rand bit [7:0]  payload[];
  rand bit [11:0] vlan_id;
  bit  [31:0]     fcs;          // NOT rand — derived, not solved
  bit             vlan_en;      // state knob set by the test

  constraint c_size { payload.size() inside {[46:1500]}; }

  function void pre_randomize();
    // setup the solver's world BEFORE it runs
    if (!vlan_en) vlan_id.rand_mode(0);   // freeze field for this solve
  endfunction

  function void post_randomize();
    // derive what the solver cannot: a function of the random result
    fcs = crc32(payload);
  endfunction
endclass

What happens at f.randomize(): pre_randomize runs and possibly removes vlan_id from the solve set; the solver then picks payload (size and contents) in one solve; on success post_randomize computes fcs from the actual randomized payload. If the solve fails, post_randomize is not called — fcs would otherwise be derived from stale data. pre_randomize runs unconditionally, before success is known.


Call order across inheritance and nesting

Two ordering rules matter. Inheritance: the hooks behave virtually in practice — the most-derived override runs, and it should call super.pre_randomize() / super.post_randomize() to preserve base-class behavior. Nesting: when the solver recurses into rand class handles, each contained object's hooks run around the overall solve — pre hooks top-down before solving, post hooks after the global solve succeeds.

diagram
CALL ORDER for  pkt.randomize()
  (class derived_pkt extends base_pkt; rand payload pl inside)

  ┌────────────────────────────────────────────────────────┐
  │ 1. pkt.pre_randomize()                                 │
  │      derived override runs; calls super.pre_randomize()│
  │ 2. pl.pre_randomize()          (nested objects next)   │
  ├────────────────────────────────────────────────────────┤
  │ 3.        ONE GLOBAL SOLVE                              │
  │           pkt fields + pl fields together              │
  │           (constraints may span both objects)          │
  ├────────────────────────────────────────────────────────┤
  │   on success only:                                     │
  │ 4. pkt.post_randomize()                                │
  │ 5. pl.post_randomize()                                 │
  └────────────────────────────────────────────────────────┘

  on solve FAILURE: steps 4–5 skipped, fields unchanged, returns 0
  (pre hooks already ran — any side effects they made persist!)

The virtual-ness folklore

Older material claims pre/post_randomize are “not virtual”. The reality: IEEE 1800 defines them as built-in virtual-behaving hooks — when randomize() invokes them, the most-derived override executes, exactly as you would expect from virtual dispatch. The pre-2012 confusion arose because early LRM wording did not use the keyword 'virtual' in their prototypes, and some early tools dispatched them non-virtually. Modern simulators all dispatch to the derived override. The interview-safe answer: “they dispatch virtually in all modern tools per 1800-2012 clarifications; declare them without the virtual keyword, override freely, and call super”.

systemverilog
class base_pkt;
  rand bit [7:0] kind;
  function void post_randomize();
    $display("base post: kind=%0d", kind);
  endfunction
endclass

class derived_pkt extends base_pkt;
  bit [7:0] kind_log[$];
  function void post_randomize();
    super.post_randomize();        // keep base behavior — easy to forget
    kind_log.push_back(kind);
  endfunction
endclass

base_pkt h = new derived_pkt();    // base handle, derived object
initial begin
  void'(h.randomize());
  // prints "base post" AND logs — derived override ran via base handle
end

What happens: randomize() on the base-typed handle still dispatches to derived_pkt::post_randomize — proof of virtual behavior. Omitting the super call is the classic bug: the base class's derivations (CRC, logging) silently stop happening in every derived transaction.


Typical uses — and the recursion trap

  • pre_randomize: select weight/mode knobs, flip rand_mode()/constraint_mode() for this solve, capture pre-state for later comparison.

  • post_randomize: derive non-rand fields (CRC, parity, ECC), enforce relationships too procedural for constraints, log or histogram the chosen values.

  • post_randomize fix-ups vs constraints: prefer constraints for anything the solver CAN express — post fix-ups bypass the solver, so other constraints cannot react to the fixed-up value.

systemverilog
class msg;
  rand bit [7:0] data[4];
  bit            parity;

  function void post_randomize();
    parity = ^{data[0], data[1], data[2], data[3]};   // derive: fine

    // THE TRAP — re-randomizing inside the hook:
    // if (parity) void'(this.randomize());
    // randomize() → post_randomize() → randomize() → ... recursion!
    // Even when guarded, each inner call reruns hooks and re-solves
    // everything, destroying the values the outer call just picked.
  endfunction
endclass

What goes wrong with randomize-inside-the-hook: the inner randomize() triggers the full sequence again — pre, solve, post — so post_randomize re-enters itself; without a termination condition this recurses until stack exhaustion, and with one it still silently replaces the outer solve's results and advances randc cycles. If a result needs rejection-and-retry, loop at the call site , or better, express the requirement as a constraint so the solver never produces a rejected value: constraint c_par { (^data[0] ^ ... ) == 0; } style.


Interview angle

These hooks are a favorite for layered questions because they connect solver mechanics, inheritance, and coding discipline in one topic.

  • “When does post_randomize NOT run?” — when the solve fails (returns 0). pre_randomize always runs.

  • “Where do you compute a CRC over random payload?” — post_randomize: it is a function of the solved values, so it cannot be a constraint input the same call.

  • “Why not fix illegal results in post_randomize instead of writing constraints?” — fix-ups bypass the solver: other constraints cannot see the fixed value, distribution is distorted, and randomize(null) checks will disagree with what you actually send.

  • “Are the hooks virtual?” — they dispatch to the most-derived override in all modern tools; always call super in overrides.

  • “What happens if you call randomize() inside post_randomize?” — re-entrant hook execution; recursion risk, clobbered results, burned randc slots. Retry at the call site instead.

systemverilog
// Call-site retry pattern (instead of hook recursion)
msg m = new();
int tries = 0;
initial begin
  do begin
    assert (m.randomize());
    tries++;
  end while (!external_filter_ok(m) && tries < 100);
  if (tries == 100) $error("could not produce acceptable msg");
end
// Better still: encode external_filter_ok's rule as a constraint
// so the solver only ever produces acceptable messages.

Key takeaways

  • pre_randomize sets up state the solver reads; post_randomize derives values from the solved result — and runs only on success.

  • Hooks dispatch to the most-derived override (virtual behavior in modern tools); always call super.pre/post_randomize().

  • For nested rand objects there is ONE global solve, with each object's pre hooks before it and post hooks after it.

  • Never call randomize() inside the hooks — recursion, clobbered results, advanced randc cycles. Retry at the call site or constrain it away.

Common pitfalls

  • Deriving CRC/parity in post_randomize but forgetting it is skipped on solve failure — stale derived fields follow stale random fields.

  • Overriding a hook without calling super — base-class derivations and logging silently vanish in derived transactions.

  • Using post_randomize fix-ups for rules the solver could enforce — distribution skew and constraint/check disagreement.

  • Side effects in pre_randomize (e.g. rand_mode(0)) that persist after a FAILED solve — the next call inherits the modified mode unexpectedly.

  • Calling randomize() from inside pre/post_randomize — re-entrancy and recursion.