Part 3 · Constraint Randomization · Intermediate
Randomization Failure Taxonomy
The four failure modes — hard contradiction, silent skip, wrong distribution, irreproducibility — and the triage flowchart.
Two symptoms, four diseases
From the caller's perspective, randomize() can only do two things: return 0 (the solver proved no solution exists under the active constraints) or return 1 and assign values. But the underlying failure modes split into four distinct categories, and each demands a different debugging tool. Treating them as one problem — “my randomization is broken” — is how engineers waste days.
The four failure modes
Hard contradiction — randomize() returns 0 because the active constraint set is unsatisfiable. The solver is not broken; your constraints genuinely conflict, possibly only for the current state values.
Silent skip — randomize() returns 1, but a field you expected to change did not. Causes: the nested object handle is null (solver silently skips it), rand_mode(0) was left set, or the field was never declared rand.
Wrong distribution — randomize() succeeds every time, values are legal, but the spread is skewed: a corner case never appears, or one value dominates. Causes live in implication structure, missing solve...before, eaten soft constraints, or width/sign truncation.
Irreproducibility — a failure appeared in one regression run and you cannot make it happen again. This is a seed-management and random-stability problem, not a constraint problem.
The critical first question is always: did randomize() return 0, or did it return 1 with wrong values? Those two branches share almost no debugging steps. Return-0 problems are solved by isolating the contradicting constraint subset. Return-1 problems are solved with histograms, handle checks, and seed replay.
The triage flowchart
RANDOMIZATION FAILURE TRIAGE
randomization "looks wrong"
|
v
Did you CHECK the return value? --no--> fix that first:
| assert(obj.randomize());
| yes (bare calls hide failures)
v
return value == 0 ?
| |
yes no (== 1, but values wrong)
| |
v v
HARD CONTRADICTION Did the field change at all
- same constraints across many calls?
every time? | |
-> static conflict no yes
- only with certain | |
state/inline args? v v
-> state-dependent SILENT SKIP WRONG DISTRIBUTION
-> go to: - handle null? - collect histogram
"Finding - rand_mode 0? - check implications,
Contradictions" - not rand? solve...before,
-> go to: soft, width/sign
"Silent -> go to: "Wrong
Failures" Distribution"
|
v
Can you reproduce it on demand?
| |
yes no --> SEED PROBLEM
| capture + replay seed first
v -> go to: "Reproducing
debug it Random Failures"Note the diamond at the bottom: reproduction is orthogonal to the other three modes. A hard contradiction that only fires on seed 4711 must first be made deterministic before constraint-level debugging is even possible. Experienced engineers lock down the seed before touching any constraints.
Why the solver fails: a 60-second refresher
The constraint solver gathers every active constraint — class constraints up the inheritance chain, constraints on nested rand objects, inline with constraints, and the implicit constraints from each variable's declared type and width — into one simultaneous system. Solving is bidirectional : there is no left-to-right evaluation order, so a constraint written as a check can act as a generator and vice versa. State variables (non-rand fields) enter as constants at their current values. If the conjunction of all of that has no solution, randomize() returns 0 and every rand field keeps its previous value — nothing is partially assigned.
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 t;
initial begin
pkt p = new();
p.small_mode = 1;
// Solvable: len in [1:3], payload_max in [16:255]
assert(p.randomize());
// Now an inline constraint creates a state-dependent contradiction:
// small_mode==1 forces len<4, inline forces len>=10 -> UNSAT
if (!p.randomize() with { len >= 10; })
$display("randomize failed as expected: len>=10 vs len<4");
// IMPORTANT: p.len still holds the value from the EARLIER success.
end
endmoduleWalk through the failure: the inline constraint does not replace class constraints — it is ANDed with them. With small_mode == 1, the implication small_mode -> len < 4 is active, and len >= 10 makes the system unsatisfiable. The same call would succeed with small_mode == 0. This is the canonical state-dependent contradiction — the constraint set is fine, the state makes it infeasible. Also note the last comment: after a failed randomize, the object silently retains stale values, which is why an unchecked call is so dangerous.
The assert(randomize()) discipline
A bare obj.randomize(); call discards the return value. On failure the simulation continues with stale field values — the previous transaction's data gets driven again, the scoreboard sees a duplicate, and you debug a phantom protocol bug three layers away from the real cause. Every randomize call must be checked.
// Level 1 — minimum acceptable: immediate assertion
assert(txn.randomize()) else
$fatal(1, "txn.randomize() failed at %0t", $time);
// Level 2 — better: check + context dump
if (!txn.randomize()) begin
$display("RAND-FAIL state: mode=%0d max=%0d", txn.small_mode, txn.payload_max);
txn.print(); // or $display("%p", txn);
$fatal(1, "txn randomize failed");
end
// Level 3 — what NOT to do
void'(txn.randomize()); // explicit discard: failure is invisible
txn.randomize(); // many tools warn; same invisibilityOne subtlety worth knowing for interviews: some methodologies discourage plain assert() for this because assertions can be globally disabled with $assertoff, which would silently disable your failure check too. A fatal-on-failure wrapper task (shown in the Silent Failures lesson) is immune to that.
Interview angle
When asked “what happens when randomize() fails?”, the strong answer has three parts: (1) it returns 0 and all rand fields keep their previous values — no partial assignment; (2) therefore an unchecked call silently reuses stale data, which is why assert(randomize()) is a non-negotiable coding rule; (3) the failure means the conjunction of all active constraints — class, inherited, nested-object, inline, plus current state-variable values — is unsatisfiable, which immediately suggests the binary-search isolation technique covered next. Naming the four failure modes unprompted signals genuine field experience.
Key takeaways
Classify first: return 0 (contradiction) vs return 1 with wrong values (skip, skew, or seed issue).
On failure, rand fields keep prior values — partial assignment never happens.
Inline with constraints are ANDed with class constraints, never substituted.
State variables enter the solver as constants — a constraint set can be UNSAT only for certain states.
Check every randomize() return value; make the failure loud and immediate.
Common pitfalls
Calling randomize() bare — failure leaves stale values and the bug surfaces far downstream.
Assuming the solver is broken — in practice the constraint set (or state) is genuinely contradictory.
Debugging constraints before locking the seed — the failure mutates or vanishes between runs.
Forgetting that inline with adds constraints — engineers expect it to override the class constraint.
Relying on assert() when $assertoff may be in effect — use a fatal wrapper in production benches.