From the caller side, randomize can only return 0 or return 1 with values. The underlying failure modes split into four categories, and each demands a different debugging tool.
The critical first question is always whether randomize returned 0 or returned 1 with wrong values. Those two branches share almost no debugging steps.
Mode | Symptom | Debug with |
|---|---|---|
Hard contradiction | Returns 0 every time or only in some states | Constraint block binary search |
Silent skip | Returns 1, but a field never changes | Null-handle and rand_mode audit |
Wrong distribution | Returns 1, values legal but skewed | Histogram or covergroup on rand fields |
Irreproducibility | Failure appears once and cannot be replayed | Seed capture and stability rules |
Run the symptom through this chart before opening any constraint file. Reproduction sits at the bottom because it is orthogonal to the other three modes.
A hard contradiction that only fires on one seed must first be made deterministic. Experienced engineers lock down the seed before touching any constraints.
The solver gathers every active constraint into one simultaneous system: class constraints up the inheritance chain, nested-object constraints, inline with constraints, and each variable's type and width. Solving is bidirectional, so a constraint written as a check can act as a generator.
State variables enter as constants at their current values. If the conjunction has no solution, randomize returns 0 and every rand field keeps its previous value.
class pkt;
rand bit [7:0] len;
rand bit [7:0] payload_max;
bit small_mode; // state variable, NOT rand
constraint len_c { len > 0; len <= payload_max; }
constraint mode_c { small_mode -> len < 4; }
constraint floor_c { payload_max >= 16; }
endclass
module top;
initial begin
pkt p = new();
p.small_mode = 1;
// Solvable: len in [1:3], payload_max in [16:255].
if (!p.randomize()) $fatal(1, "pkt randomize failed");
// State-dependent contradiction: small_mode==1 forces len<4,
// the inline forces len>=10, so the conjunction is empty.
if (!p.randomize() with { len >= 10; })
$display("randomize did not solve: len>=10 vs len<4");
else $fatal(1, "FAIL: len>=10 should contradict small_mode");
// p.len still holds the value from the EARLIER success.
$display("PASS: the state-dependent contradiction failed as analyzed");
$finish;
end
endmodule%Warning-UNSATCONSTR: /var/folders/41/dgs80y1569b4x8pwflv_w9jr0000gn/T/rtbtb-K1WcxI/t42.sv:7: Unsatisfied constraint: 'constraint mode_c { small_mode -> len < 4; }'
%Warning-UNSATCONSTR: /var/folders/41/dgs80y1569b4x8pwflv_w9jr0000gn/T/rtbtb-K1WcxI/t42.sv:20: Unsatisfied constraint: 'if (!p.randomize() with { len >= 10; })'
randomize did not solve: len>=10 vs len<4
PASS: the state-dependent contradiction failed as analyzedThe inline constraint does not replace class constraints. It is ANDed with them, and the same call would succeed with small_mode equal to 0.
A bare obj.randomize call discards the return value. On failure the simulation continues with stale field values, and the bug surfaces far downstream as a phantom protocol mismatch.
Some teams avoid plain assert for this because assertions can be disabled globally. Worse, a disabled assert may skip the randomize call entirely, not just leave it unchecked. A fatal-on-failure wrapper, shown in the checklist lesson, is immune to that. The listing below shows the three levels.
Three ways to call it, from best to worst. This fragment is an illustration, so the course checks skip it.
// snippet
class txn;
rand bit [7:0] len;
constraint c_len { len inside {[1:64]}; }
endclass
module top;
initial begin
txn t;
t = new();
// Level 1, minimum: an immediate assertion. If assertions are
// turned off, the tool may skip the randomize call entirely.
assert (t.randomize());
// Level 2, preferred: a plain check that dumps context and stops.
if (!t.randomize()) begin
$display("txn randomize failed: len=%0d (stale value)", t.len);
$fatal(1, "txn randomize failed");
end
// Level 3, never: the status is thrown away.
void'(t.randomize());
end
endmoduleOn Verilator 5.048, post_randomize also runs after a failed call, which the standard forbids. A post_randomize that logs or forwards the transaction would then pass stale values downstream with no warning, so the return check has to come first.
Their previous values. Nothing is partially assigned, which is why the object dump at a failure site shows stale rand values next to live state values.
Neither. Both have equal authority and the call is UNSAT. Only a soft constraint creates priority, and a conflicting soft constraint is dropped entirely.
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 ran post_randomize after a failed call too, which the standard forbids.