Every class may define pre_randomize and post_randomize hooks. randomize invokes them automatically: pre runs before the solver, post runs after it succeeds.
Their division of labor follows from the solver contract. Pre sets up state the solver reads, such as mode flags and rand mode switches. Post derives values the solver cannot produce, such as checksums over the solved payload.
// not-run: class only, no module, and crc32() is not defined here
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
vlan_id.rand_mode(vlan_en); // freeze vlan_id unless enabled; the mode
// is sticky, so set it on every call
endfunction
function void post_randomize();
// derive what the solver cannot: a function of the random result
fcs = crc32(payload);
endfunction
endclassPre runs unconditionally, before success is known. Post runs only on success: deriving from stale data after a failed solve would corrupt the frame.
Hook | Runs | Job |
|---|---|---|
pre_randomize | Always, even on failure | Set up solver-readable state: knobs, rand mode and constraint mode switches |
post_randomize | Only on success | Derive non-rand fields from solved values: CRC, parity, timestamps, logs |
Hooks dispatch to the most-derived override in all modern tools, so each override should call its super hook. When the solver recurses into rand handles, there is still one global solve for the whole object tree.
Each participating object's post hook runs once after that solve succeeds. The standard does not fix their relative order, so portable code never relies on it.
The bench at the end of this lesson runs exactly this: a derived post_randomize called through a base handle.
Dispatching through a base-typed handle to the derived override proves the virtual behavior. Omitting the super call is the classic bug.
Without it, base derivations silently stop happening in every derived transaction.
// snippet: the trap, shown, never run
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 re-enters the hooks,
// re-solves everything, and burns randc slots. Never do this:
// if (parity) assert (this.randomize());
endfunction
endclassAn inner randomize triggers the full sequence again, so post re-enters itself. Without a guard this recurses, and with one it still replaces the outer results.
Retry at the call site instead, or express the rule as a constraint.
// snippet: msg is the class above; external_filter_ok() stands for any outside check
// Call-site retry pattern (instead of hook recursion):
module top;
msg m = new();
int tries = 0;
bit ok;
initial begin
do begin
assert (m.randomize());
tries++;
ok = external_filter_ok(m);
end while (!ok && tries < 100);
// Test ok, not tries == 100: the 100th try may have succeeded.
if (!ok) $error("could not produce acceptable msg");
end
endmodule
// Better still: encode the filter rule as a constraint,
// so the solver only ever produces acceptable messages.A derived post_randomize reached through a base handle, a derivation the solver cannot make, and a failed solve.
h.randomize() three times through a base handle, then once with a constraint that contradicts c_kind. How many entries does kind_log hold, and does kind change on the last call?Three, one per successful solve, because the derived post hook calls super and then logs. The last call fails, so kind keeps its value.
class base_pkt;
rand bit [7:0] kind;
bit [7:0] data_par;
rand bit [7:0] data[4];
int pre_n, post_n;
constraint c_kind { kind < 16; }
function void pre_randomize();
pre_n++;
endfunction
function void post_randomize();
post_n++;
data_par = data[0] ^ data[1] ^ data[2] ^ data[3]; // derive: the solver cannot
endfunction
endclass
class derived_pkt extends base_pkt;
bit [7:0] kind_log[$];
function void post_randomize();
super.post_randomize(); // keep the base derivation
kind_log.push_back(kind);
endfunction
endclass
module tb_hooks;
base_pkt h;
derived_pkt d;
bit [7:0] keep;
int ok;
initial begin
d = new();
h = d; // base handle, derived object
repeat (3) if (!h.randomize()) $fatal(1, "FAIL: randomize failed");
$display("after 3 solves: pre=%0d post=%0d logged=%0d parity ok=%0d", d.pre_n, d.post_n, d.kind_log.size(),
d.data_par == (d.data[0] ^ d.data[1] ^ d.data[2] ^ d.data[3]));
if (d.pre_n != 3 || d.post_n != 3 || d.kind_log.size() != 3) $fatal(1, "FAIL: hooks did not run once per solve");
if (d.data_par != (d.data[0] ^ d.data[1] ^ d.data[2] ^ d.data[3])) $fatal(1, "FAIL: post did not derive the parity");
keep = d.kind;
ok = h.randomize() with { kind > 100; }; // contradicts c_kind
$display("contradiction: returned %0d, kind %0d -> %0d, pre=%0d post=%0d", ok, keep, d.kind, d.pre_n, d.post_n);
if (ok != 0 || d.kind != keep) $fatal(1, "FAIL: a failed solve changed kind");
$display("PASS: derived post ran through a base handle and kept the base parity; the failed solve changed nothing");
$finish;
end
endmoduleafter 3 solves: pre=3 post=3 logged=3 parity ok=1
%Warning-UNSATCONSTR: /var/folders/41/dgs80y1569b4x8pwflv_w9jr0000gn/T/rtbtb-ZYw5Pz/t118.sv:6: Unsatisfied constraint: 'constraint c_kind { kind < 16; }'
%Warning-UNSATCONSTR: /var/folders/41/dgs80y1569b4x8pwflv_w9jr0000gn/T/rtbtb-ZYw5Pz/t118.sv:38: Unsatisfied constraint: 'ok = h.randomize() with { kind > 100; }; '
contradiction: returned 0, kind 0 -> 0, pre=4 post=4
PASS: derived post ran through a base handle and kept the base parity; the failed solve changed nothingThe standard says post_randomize does not run when the solve fails. One of the simulators used here runs it anyway, which is why the bench checks the fields after the failed call rather than the hook count.
Pre ran and post did not. Random fields are unchanged and the CRC is stale, since post only derives on success.
Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown. Verilator prints a harmless WIDTHTRUNC warning for if (!obj.randomize()), because randomize() returns an int, and an UNSATCONSTR warning when a solve fails on purpose. Icarus 13 does not support constraints.
Verilator 5.048 runs post_randomize after a failed solve too (the standard says it must not), so the bench checks the fields after the failure, not the hook count.