SystemVerilog · Module 10
pre_randomize & post_randomize Callbacks
Setup-before-solve vs derive-and-log-after-success; mandatory super chaining; post_randomize skipped on failure; canonical UVM patterns for sub-object construction, scenario gating, and CRC computation.
Module 10 · Page 10.12
The Call Sequence — Where Each Hook Fires
Both hooks are built-in virtual functions inherited by every class. They have empty default implementations — you override only the ones you need. The full sequence on every randomize() call is:
| Step | What runs | Called even on failure? |
|---|---|---|
| 1 | pre_randomize() — your hook before the solver | Yes — always |
| 2 | Constraint solver finds valid values (or fails) | N/A |
| 3 | post_randomize() — your hook after success | No — only called when solver succeeds |
| 4 | randomize() returns 1 (success) or 0 (failure) | N/A |
Syntax — Overriding the Hooks
Both hooks are declared as virtual function void. Override them inside your class body with the same signature. No arguments, no return value.
class Packet;
rand bit [7:0] data[4];
rand bit [1:0] prio;
bit [7:0] crc; // NOT rand — computed in post_randomize
int call_count; // NOT rand — tracked in pre_randomize
// ── Called BEFORE the solver ───────────────────────────────────
virtual function void pre_randomize();
call_count++;
$display("[pre] randomize() call #%0d", call_count);
endfunction
// ── Called AFTER the solver succeeds ──────────────────────────
virtual function void post_randomize();
crc = data[0] ^ data[1] ^ data[2] ^ data[3]; // simple XOR CRC
$display("[post] prio=%0d crc=0x%02h", prio, crc);
endfunction
endclasspre_randomize() — What to Put Here
pre_randomize() runs with the object in its current state — before any field is updated. It is the right place for anything that must happen before the solver sees the constraint set.
Constraint steering
Turn constraints on/off based on external state or a test counter before each solve. More readable than doing it outside the class.
Setting non-random bounds
Update plain (non-rand) variables that are referenced as constants inside constraint blocks — so the solver sees the fresh values on this call.
Transaction sequencing
Increment a call counter, advance a state machine, or derive a seed for the next transaction based on where you are in the test sequence.
Logging / debug
Print the pre-randomise state, record which constraints are active, or set a debug flag before the solver runs.
class PhasedTxn;
rand bit [31:0] addr;
int phase = 0; // set by the test, not by the solver
constraint phase0_range { addr inside {[32'h0000_0000:32'h0FFF_FFFF]}; }
constraint phase1_range { addr inside {[32'h1000_0000:32'h1FFF_FFFF]}; }
virtual function void pre_randomize();
// Only one range constraint active at a time — no contradiction
phase0_range.constraint_mode(phase == 0);
phase1_range.constraint_mode(phase == 1);
endfunction
endclass
PhasedTxn t = new();
t.phase = 0; assert(t.randomize()); // addr in 0x0..0x0FFF_FFFF
t.phase = 1; assert(t.randomize()); // addr in 0x1..0x1FFF_FFFFpost_randomize() — What to Put Here
post_randomize() runs after the solver has successfully assigned values to all rand fields. The solved values are readable and stable. It is the right place for anything that must happen using the newly randomised values.
Computed fields
CRC, checksum, parity, length, header packing — any field whose value is derived from the randomised fields but cannot be expressed as a constraint.
Type conversion
Convert a solved integer field into a string, enum, or packed struct. Randomise sub-objects that depend on the solved scalar fields (see page 10.11).
Display / coverage sampling
Print the randomised transaction, update statistics, or trigger a covergroup sample — guaranteed to run only on successful solves.
Post-solve validation
Assert extra invariants that are hard to express as constraints — for example, verifying that a packed CRC field matches computed CRC.
class EtherFrame;
rand bit [47:0] src_mac;
rand bit [47:0] dst_mac;
rand bit [15:0] ethertype;
rand bit [7:0] payload[];
// NOT rand — computed after solve
bit [31:0] fcs; // Frame Check Sequence
int frame_len; // total frame length in bytes
constraint payload_sz { payload.size() inside {[46:1500]}; }
constraint no_loopback { src_mac != dst_mac; }
virtual function void post_randomize();
// Compute frame length (headers + payload + FCS placeholder)
frame_len = 6 + 6 + 2 + payload.size() + 4;
// Simple FCS placeholder (real CRC32 would go here)
fcs = 32'h0;
foreach (payload[i]) fcs ^= payload[i];
$display("[Frame] src=%h dst=%h len=%0d fcs=%08h",
src_mac, dst_mac, frame_len, fcs);
endfunction
endclassInheritance — super Calls Are Essential
When a child class overrides either hook, it is responsible for calling the parent's version using super.pre_randomize() or super.post_randomize(). If you omit the super call, the parent's hook logic is silently skipped — including any CRC computation, logging, or constraint steering the parent implements.
class Base;
rand bit [7:0] x;
bit [7:0] parity;
virtual function void post_randomize();
parity = ^x; // base computes parity
$display("Base: x=%h parity=%b", x, parity);
endfunction
endclass
class Child extends Base;
rand bit [7:0] y;
bit [7:0] checksum;
virtual function void post_randomize();
super.post_randomize(); // ← MUST call parent first
checksum = x + y; // child adds its own derived field
$display("Child: y=%h checksum=%h", y, checksum);
endfunction
endclass
// Without super.post_randomize():
// parity is never computed — Base's logic is silently lostComplete Practical Example
A real transaction class combining pre_randomize for phase-based constraint steering and post_randomize for CRC computation, object randomisation, and display.
class CanFrame;
// Randomised fields
rand bit [10:0] id;
rand bit rtr;
rand bit [3:0] dlc; // data length code (0–8)
rand bit [7:0] data[]; // dynamic — size driven by dlc in post
// Derived fields (computed)
bit [14:0] crc15;
bit [7:0] frame_count;
// External state set by the test
bit extended_id_only = 0;
constraint dlc_max { dlc <= 8; }
constraint no_data_on_rtr { rtr -> dlc == 0; }
constraint std_id { id < 11'h400; } // standard 11-bit
constraint ext_id { id >= 11'h400; } // extended range
virtual function void pre_randomize();
// Steer ID range before solve
std_id.constraint_mode(!extended_id_only);
ext_id.constraint_mode( extended_id_only);
endfunction
virtual function void post_randomize();
// Size the payload array to match solved DLC
data = new[dlc];
foreach (data[i]) data[i] = $urandom_range(0, 255);
// Compute a simple CRC15 placeholder
crc15 = 0;
foreach (data[i]) crc15 ^= data[i][7:1];
frame_count++;
$display("[CAN #%0d] id=%03h rtr=%b dlc=%0d crc=%04h",
frame_count, id, rtr, dlc, crc15);
endfunction
endclassCommon Pitfalls
Calling randomize() from inside a hook
Calling randomize() from inside pre_randomize() or post_randomize() on the same object causes infinite recursion. Use std::randomize() for local variables, or call randomize() on a different object.
Forgetting super in a child class
Omitting super.post_randomize() silently discards the parent's CRC, logging, and any other post-solve logic. Always call super unless you deliberately intend to replace the parent's behaviour entirely.
Reading rand fields in pre_randomize
In pre_randomize(), rand fields still hold their previous values from the last call. Do not base constraint steering on them expecting the new values — the solver hasn't run yet.
Relying on post_randomize after a failed solve
If the solver fails and you use void'(randomize()), post_randomize() is never called, but your derived fields (CRC, checksum) still hold stale values from the previous successful call. Always check the return value.
Quick Reference
// ── Declaration ───────────────────────────────────────────────
virtual function void pre_randomize(); // override to run before solver
virtual function void post_randomize(); // override to run after solver
// ── Call sequence ─────────────────────────────────────────────
// pre_randomize() → always fires (even on failure)
// solver runs → may fail (returns 0)
// post_randomize() → fires ONLY on success
// ── Inheritance — always call super ───────────────────────────
virtual function void post_randomize();
super.post_randomize(); // parent's derived fields first
// child's additions here
endfunction
virtual function void pre_randomize();
// child's prep here
super.pre_randomize(); // parent's base prep last
endfunction
// ── Good uses ─────────────────────────────────────────────────
// pre: constraint_mode steering, non-rand bounds update, logging
// post: CRC/checksum, length computation, sub-object randomise, display
// ── Bad uses ──────────────────────────────────────────────────
// ✗ Do NOT call obj.randomize() on same object (infinite recursion)
// ✗ Do NOT read rand fields in pre expecting new values (not solved yet)Verification Usage — Where pre/post Hooks Live in Production UVM
Across every UVM environment, pre_randomize and post_randomize cluster around five reliable patterns. Each addresses a specific need that pure constraint expressions cannot handle.
pre_randomize: sub-object construction
For classes containing rand arrays of class handles, pre_randomize is where the elements are allocated. The solver randomises fields of already-constructed objects; it does not construct.
<code>class burst_xact extends uvm_sequence_item;
rand int beat_count;
rand beat_xact beats[];
function void pre_randomize();
super.pre_randomize();
if (beats == null) beats = new[beat_count];
foreach (beats[i])
if (beats[i] == null)
beats[i] = beat_xact::type_id::create($sformatf("b_%0d", i));
endfunction
endclass</code>pre_randomize: scenario-driven constraint mode
Toggle constraint blocks before each call based on test-layer scenario state. Common in error-injection scenarios where one transaction in N violates a specific rule.
<code>function void pre_randomize();
super.pre_randomize();
if (test_cfg.error_injection_active && ($urandom_range(0, 9) == 0)) begin
c_align.constraint_mode(0); // 10% of transactions misaligned
end else begin
c_align.constraint_mode(1);
end
endfunction</code>post_randomize: CRC and checksum computation
Most protocols carry a CRC, checksum, or parity computed from the randomised payload. These derived fields must match the payload — perfect post_randomize territory.
<code>function void post_randomize();
super.post_randomize();
crc = compute_crc32(payload);
parity = ^payload;
endfunction</code>post_randomize: building associative arrays from randomised lists
Associative arrays can't be randomised directly. The idiom: randomise parallel key/value lists, then assemble the associative array procedurally.
<code>function void post_randomize();
super.post_randomize();
mem.delete();
foreach (keys[i]) mem[keys[i]] = vals[i];
endfunction</code>post_randomize: logging the constrained outcome
The canonical place to log what the solver picked — transaction ID, field values, soft-default respect flags. The log line is invaluable for post-hoc failure forensics.
Simulation Behavior — When Each Hook Fires
Exact call sequence
The randomize() method conceptually executes: (1) call pre_randomize(); (2) run the constraint solver; (3) if solve succeeded, write the rand fields and call post_randomize(); (4) return 1 (success) or 0 (failure). On failure, step 3 is skipped entirely.
pre_randomize always runs
It runs before the solver attempts to find a satisfying assignment — independent of whether the solver eventually succeeds. Code in pre_randomize executes on every randomize call, period.
post_randomize conditional on success
Only invoked when the solver returns 1. On failure (solver returned 0), post_randomize is skipped, and any derived-field computation it contains does not execute. The fields hold their previous values (or defaults on the first failure).
Synchronous execution on host CPU
Both hooks execute synchronously on the host CPU. $time does not advance during them. They are pure procedural code — function calls, assignments, control flow — just like any class method. No simulation-time events fire during their execution.
Inheritance and virtual dispatch
Both hooks are declared virtual function void. Child class overrides participate in normal virtual dispatch — a parent-handle call parent_h.randomize() on a child object invokes the child's override. Each override must explicitly call super.<same-method>() to chain to the parent's hook.
<code>function void pre_randomize();
super.pre_randomize(); // chain to parent
if (beats == null)
beats = new[expected_size]; // setup sub-objects
endfunction
function void post_randomize();
super.post_randomize(); // chain to parent
crc = compute_crc(payload); // derive
`uvm_info("XACT",
$sformatf("addr=0x%h", addr),
UVM_HIGH)
endfunction</code><code>function void pre_randomize();
// No super chain — parent's setup lost
$display("addr=%h", addr);
// BUG: addr is the OLD value (previous
// call) — solver hasn't run yet
endfunction
function void post_randomize();
// No super chain — parent's CRC lost
if (!something) return;
// BUG: conditional skips parent's derive,
// next test step expects it
endfunction</code>Waveform Analysis — Making Hook Effects Visible
The hooks themselves don't fire waveform events — they're procedural functions. The way to verify they're behaving correctly is to log their effects: which derived fields got computed, which sub-objects got allocated, which constraint modes got set.
The pre/post instrumentation pattern
<code>class apb_xact extends uvm_sequence_item;
rand bit [31:0] addr, data;
bit [31:0] crc;
function void pre_randomize();
super.pre_randomize();
`uvm_info("PRE",
$sformatf("scenario=%0d c_align=%s",
scenario_id,
c_align.constraint_mode() ? "ON" : "OFF"),
UVM_HIGH)
endfunction
function void post_randomize();
super.post_randomize();
crc = compute_crc32(data);
`uvm_info("POST",
$sformatf("addr=0x%h data=0x%h crc=0x%h",
addr, data, crc),
UVM_HIGH)
endfunction
endclass</code>ASCII view — hook events bracketing each randomize
<code> 90ns PRE scenario=2 c_align=ON
100ns POST addr=0x4000_0010 data=0xDEAD_BEEF crc=0xA1B2_C3D4
190ns PRE scenario=3 c_align=OFF ← scenario flipped
200ns POST addr=0x4000_0011 data=0x12345678 crc=0x9876_5432
290ns PRE scenario=2 c_align=ON
...
↑ ↑
pre log shows setup post log shows constrained values
the hook performed AND derived CRC</code>Diagnosing "derived field is wrong" failures
When a DUT NAKs a transaction for "bad CRC," the immediate question is: was the CRC computed at all? With post_randomize logging, you see either "post fired, CRC=X" (computation ran, X is the value) or no post line (solve failed, CRC stayed at default). Either case directs the debugging — to the CRC algorithm if the value is wrong, or to the solver failure if post never fired.
Industry Insights — Hard-Won Lessons About pre/post Hooks
Debugging Academy — Five Real Bugs From Hook Misuse
"pre_randomize reads rand field expecting solver values"
DEBUGA debug print in pre_randomize shows the same value every call; the developer thinks the solver is broken.
<code>class xact;
rand bit [31:0] addr;
constraint c { addr inside {[0:255]}; }
function void pre_randomize();
$display("[pre] addr=0x%h", addr); // BUG: pre sees previous-call value
endfunction
function void post_randomize();
$display("[post] addr=0x%h", addr); // post sees current solve
endfunction
endclass
// Log: pre and post show different values, confusing the developer</code>pre_randomize runs before the solver; fields hold values from the previous solve (or defaults on first call). The values the developer expects to see only exist after post_randomize.
Move the logging to post_randomize. pre_randomize is for setup, not inspection. The only time to read a rand field in pre_randomize is when you genuinely need its previous-call value (rare).
"super.pre_randomize() omitted; sub-objects null"
DEBUGA child class's randomize appears to skip sub-object setup; downstream code crashes on null handles.
<code>class parent_xact;
rand beat_xact beats[];
function void pre_randomize();
if (beats == null) beats = new[8];
foreach (beats[i])
if (beats[i] == null) beats[i] = beat_xact::type_id::create($sformatf("b_%0d", i));
endfunction
endclass
class child_xact extends parent_xact;
rand bit [31:0] child_field;
function void pre_randomize();
// BUG: forgot super.pre_randomize()
if ($urandom_range(0, 9) == 0)
child_field = 32'hDEAD_BEEF;
endfunction
endclass
child_xact c = new();
c.randomize();
foreach (c.beats[i]) drive(c.beats[i]); // crash — beats[i] are null</code>The child override doesn't call super.pre_randomize(), so the parent's sub-object allocation never runs. The array stays unsized; each access dereferences null.
Always call super.pre_randomize() as the first line of every child override. Same discipline as super.do_copy, super.build_phase, every UVM hook chain. Make it a style rule.
"post_randomize CRC missing on solver failure"
DEBUGA test sees occasional packets with CRC=0 despite the post_randomize computing it. Coverage shows the failures cluster around scenarios with strict constraints.
<code>class pkt;
rand bit [31:0] payload;
bit [31:0] crc;
function void post_randomize();
crc = compute_crc32(payload);
endfunction
endclass
task body();
pkt p = new();
p.randomize(); // BUG: return value discarded
drive(p); // sends a packet with crc=0 on solve failure
endtask</code>post_randomize runs only on solver success. When the solve fails, post_randomize is skipped, crc stays at its initialised value (0). The test discards the return value, so the failure is silent.
Always assert the return value: if (!p.randomize()) uvm_fatal("RAND", "solve failed"). The fatal aborts on failure with a useful diagnostic. If you can tolerate the failure and want a fallback CRC, compute it outside the hook: if (p.randomize()) drive(p); else { p.crc = compute_default_crc(); drive(p); }`.
"post_randomize silently overrides solver's choice"
DEBUGA constraint addr inside {[0:255]} appears to be ignored — addr values are seen outside the range.
<code>class xact;
rand bit [31:0] addr;
constraint c { addr inside {[0:255]}; }
function void post_randomize();
// "Fix" the address to a specific value for some scenarios
if (some_flag) addr = 32'h1000_0000; // BUG: overrides the constraint
endfunction
endclass</code>post_randomize can write to rand fields directly, overriding what the solver picked. The class now has a hidden side effect — the constraint contract isn't actually enforced. Downstream code that adds an inline with { addr inside {[64:128]}; } is confused when the post-hook overrides to 0x1000_0000.
Either (a) express the special case in the constraint set declaratively (constraint c2 { some_flag -> addr == 32'h1000_0000; }), or (b) document the post-hook override prominently at the top of the class so consumers know about it. Silent post-hoc fix-ups are class-API debt.
"Hook executes side effects but solver fails — partial state visible"
DEBUGA class's pre_randomize mutates a static counter; the developer expects "one counter increment per successful randomise" but sees increments per attempted randomise, including failures.
<code>class xact;
static int call_count = 0;
rand bit [31:0] addr;
constraint c { addr inside {[0:7]}; }
function void pre_randomize();
call_count++; // BUG: increments on every call, even failures
endfunction
endclass</code>pre_randomize runs on every randomize call regardless of outcome. Side effects (counter increments, static field updates) happen on failures too — which the developer didn't intend.
If the side effect should only happen on success, move it to post_randomize. If you need it on every attempt (e.g., counting calls), keep it in pre_randomize but make sure the rest of the code handles the failure case. The hook semantics are asymmetric — pre always fires, post only on success — and that asymmetry must be respected.
Interview Q&A — Twelve Questions You Will Be Asked
Virtual function void hooks called automatically by randomize(). pre_randomize runs before the solver attempts to find a satisfying assignment; post_randomize runs after a successful solve. Both are user-overridable; the default implementations do nothing.
Best Practices — Ten Rules for pre/post Hook Discipline
- Use
pre_randomizefor setup that affects the solver's input. Sub-object allocation, scenario-driven constraint mode, non-rand input computation. - Use
post_randomizefor computation that depends on the solver's output. CRCs, derived fields, associative-array building, logging. - Always call
super.pre_randomize()orsuper.post_randomize()as the first line of every override. Forgetting silently drops parent hook logic. - Don't read rand fields inside
pre_randomizeexpecting current solve values. The fields hold the previous call's values; inspection belongs inpost_randomize. - Remember
post_randomizeis skipped on solver failure. For derived fields that must always be valid, compute them after explicitif (!obj.randomize())checks, not inside the hook. - Don't silently override rand fields in
post_randomize. If you must, document the override prominently at the class top; better, express the override in constraints declaratively. - Document the hook contract at class top. What each hook is responsible for; what it expects to find; what it produces.
- Pair
pre_randomizeandpost_randomizelogs during bring-up. The pairing diagnoses lifecycle issues — missing PRE means no randomize call; missing POST after PRE means solver failure. - Don't put solver workarounds in
post_randomize. If the constraint set doesn't produce what you want, fix the constraint set, don't fix up the values after the fact. - Keep hook bodies short and focused. One responsibility per hook. If
post_randomizegrows beyond ~20 lines, factor into named helper methods called from the hook.