Skip to content

UVM

Constraint Quality

Whether the constraints are good — the four health properties of the solution space they define: non-empty (solvable, not contradictory), large enough (not over-constrained), correctly weighted (the solver does not guarantee uniformity), and cheap to sample (solver performance) — and why an unchecked randomize() failure silently drives stale stimulus.

Constrained-Random Verification · Module 20 · Page 20.3

The Engineering Problem

Randomization planning (Module 20.2) designed the constraints — variables, distributions, legality, steering. But a constraint set that compiles is not a good constraint set. Constraints define a solution space — the region of legal, scenario-consistent transactions the solver samples from — and that region can be unhealthy in four distinct ways, none of which the compiler catches. Contradictory constraints make the region empty: randomize() fails, and if its return is unchecked, the object keeps stale values and the testbench silently drives frozen, wrong stimulus. Over-constraining makes the region a pinhole: the solver repeats the few surviving solutions and diversity collapses. Structural skew makes the region unevenly sampled: the solver does not guarantee a uniform spread, so a "fair-looking" constraint can starve a value the coverage plan needs. And complex constraints make the region expensive to sample: each randomize() call slows, and simulation throughput craters. The problem this chapter solves is constraint quality: recognizing these four failure modes and writing constraints that are legal, solvable, well-distributed, and performant.

Constraint quality is whether the constraints define a healthy solution space — a region the solver samples — across four properties. Non-empty (solvable): the constraints must be consistent; contradictory constraints (x > 10 && x < 5) make randomize() fail (return 0), and an unchecked failure leaves stale values — a silent, frozen-stimulus bug. Large enough (not over-constrained): too many constraints shrink the region until the solver repeats a handful of solutions and diversity collapses — distinct from intended steering. Correctly weighted (no unintended skew): the solver does not guarantee a uniform distribution over values — constraint structure (sums, implications, solve...before ordering) skews which solutions are picked, so a value the coverage plan needs can starve despite a legal path; control the spread explicitly with dist and verify it. Cheap to sample (performant): complex, deeply-interacting constraint sets make the solver slow, dropping simulation throughput — keep constraints simple where possible. The governing discipline: always check randomize() success, and design the constraint set so its solution space is non-empty, roomy, intentionally-weighted, and cheap. This chapter is constraint quality: the four health properties, their failure modes, and how to write good constraints.

What makes a constraint set good — how do the four health properties of the solution space (non-empty/solvable, large enough/not over-constrained, correctly weighted/no skew, cheap/performant) define quality, why does an unchecked randomize() failure silently drive stale stimulus, and why doesn't the solver guarantee a uniform distribution?

Motivation — why a compiling constraint set can still be bad

Constraints that elaborate without error can silently produce wrong, narrow, skewed, or slow stimulus. The reasons quality is a distinct concern from correctness of intent:

  • Contradictions fail silently if unchecked. randomize() returns a status — 0 on failure — and SystemVerilog does not force you to check it. An unchecked failure leaves the object at its previous/default values, so the testbench drives stale stimulus with no error — a frozen regression that looks like it's running.
  • Over-constraining collapses diversity invisibly. Each constraint removes solutions; too many leave a handful, so the engine repeats them. Coverage plateaus far below target, and it looks like a stimulus gap when it's a starved solution space.
  • The solver doesn't promise uniformity. A common misconception is that rand produces a uniform spread over values. It produces a solution to the constraints, and the picking is skewed by constraint structure — so a value you expect to appear often may appear rarely, starving a coverage bin.
  • Constraint complexity is a runtime tax. The solver runs on every randomize() call; complex, nonlinear, deeply-ordered constraints make it slow, and at millions of transactions, a slow solver craters simulation throughput.
  • All four are invisible to the compiler. None of these — contradiction, over-constraint, skew, slowness — is a syntax error. They surface as frozen stimulus, low diversity, starved bins, or slow simssymptoms you must recognize and trace to the constraint set.

The motivation, in one line: a constraint set can compile and still be badcontradictory (fails, silently if unchecked, driving stale stimulus), over-constrained (starved diversity), skewed (the solver isn't uniform, so a needed value starves), or slow (complexity taxes every randomize) — so constraint quality is a distinct discipline: check randomize(), keep the solution space roomy, control the distribution explicitly, and keep constraints cheap.

Mental Model

Hold constraint quality as the health of the region the constraints carve, where the solver drops a dart — does the region exist, have room, get evenly covered, and is it easy to land a dart in?:

Constraints carve out a region in the space of all possible transactions — the legal, scenario-consistent ones — and the solver drops a dart into that region each time you randomize. Constraint quality is the health of that region across four questions. Does the region exist at all? Contradictory constraints carve an empty region, so the dart has nowhere to land — randomize fails, and if you don't check, you keep the last dart's position. Does it have room? Over-constraining shrinks it to a pinhole, so every dart lands in nearly the same spot — diversity collapses. Is it evenly covered? The dart isn't guaranteed to land uniformly — the region's shape pulls it toward some areas — so a corner you need can stay empty unless you deliberately weight toward it. And is it easy to land a dart in? A gnarled, twisty region is slow for the solver to find a point in, taxing every throw. Picture the space of all possible transactions. The constraints carve out a region — the legal, scenario-consistent transactions — and the solver drops a dart into that region each time you randomize. Constraint quality is the health of that region, asked as four questions. Does the region exist? Contradictory constraints (x > 10 and x < 5) carve an empty region — the dart has nowhere to land, so randomize() fails; and if you don't check the throw, you keep the last dart's position (the stale value). Does it have room? Over-constraining shrinks the region to a pinhole, so every dart lands in nearly the same spotdiversity collapses, and you re-run the same few transactions. Is it evenly covered? The dart is not guaranteed to land uniformly — the region's shape (constraint structure: sums, implications, ordering) pulls the dart toward some areas — so a corner you need can stay empty unless you deliberately weight toward it. Is it easy to land a dart in? A gnarled, twisty region (complex, nonlinear constraints) is slow for the solver to find a point in, taxing every throw — and at millions of throws, the sim crawls. A healthy region exists (non-empty), has room (not a pinhole), is evenly or intentionally covered (no accidental clustering), and is easy to sample (cheap) — and a good verifier checks that the dart actually landed (randomize() returned 1) and doesn't assume it lands evenly (verifies the spread).

So constraint quality is the health of the carved region: non-empty (the dart can land — contradictions empty it, and unchecked failures freeze the stimulus), roomy (not a pinholeover-constraining collapses diversity), evenly/intentionally covered (the dart isn't guaranteed uniform — structure skews it, so weight explicitly), and cheap to sample (not a gnarled region that slows the solver). Check that the dart landed, give the region room, weight it on purpose, and keep it easy to sample.

Visual Explanation — the four health properties of the solution space

The defining picture is the region: the constraints carve a solution space, and quality is whether that region is non-empty, roomy, correctly-weighted, and cheap to sample.

Four health properties: non-empty, large enough, correctly weighted, cheap to sampleNon-empty (solvable)the region exists — contradictory constraints make it empty, randomize() fails (check the return!)the region exists — contradictory constraints make it empty, randomize() fails (check the return!)Large enough (not over-constrained)the region has room — over-constraining shrinks it to a pinhole, the solver repeats a few solutions, diversity collapsesthe region has room — over-constraining shrinks it to a pinhole, the solver repeats a few solutions, diversity collapsesCorrectly weighted (no unintended skew)sampled as intended — the solver does NOT guarantee uniformity, so structure can starve a needed value; control with distsampled as intended — the solver does NOT guarantee uniformity, so structure can starve a needed value; control with distCheap to sample (performant)the solver finds a point quickly — complex, nonlinear, deeply-ordered constraints slow every randomize() callthe solver finds a point quickly — complex, nonlinear, deeply-ordered constraints slow every randomize() call
Figure 1 — the four health properties of the solution space. The constraints carve a region the solver samples. Non-empty: the region exists — contradictory constraints make it empty and randomize fails. Large enough: the region has room — over-constraining shrinks it to a pinhole and diversity collapses. Correctly weighted: the region is sampled as intended — the solver doesn't guarantee uniformity, so structure can skew it. Cheap to sample: the solver finds a point quickly — complex constraints slow it. A good constraint set scores well on all four; the compiler checks none of them.

The figure shows the four health properties. Non-empty (solvable) — the brand-colored top — the region exists; contradictory constraints make it empty and randomize() fails (check the return!). Large enough (not over-constrained) — the region has room; over-constraining shrinks it to a pinhole, the solver repeats a few solutions, and diversity collapses. Correctly weighted (no unintended skew) — the warning-colored layer — sampled as intended; the solver does not guarantee uniformity, so structure can starve a needed value (control with dist). Cheap to sample (performant) — the solver finds a point quickly; complex, nonlinear, deeply-ordered constraints slow every randomize() call. The crucial reading is that these are four independent properties — a constraint set can pass some and fail others — and all four must hold for quality. A region can be non-empty but a pinhole (solvable but starved). It can be roomy but skewed (diverse-in-principle but the solver clusters). It can be roomy and well-weighted but gnarled (good stimulus but slow). And the compiler checks none of them — they're semantic properties of the solution space, invisible to elaboration. The warning-colored correctly-weighted property is the most counterintuitive: engineers assume rand is uniform, but the solver only promises a legal solution, not an even spread — so a "fair" constraint can skew. The brand-colored non-empty is the most dangerous when unchecked: an empty region fails silently into stale stimulus. The diagram is the quality scorecard: non-empty (exists) + roomy (room to vary) + correctly-weighted (sampled as intended) + cheap (fast) — four properties, all required, none compiler-checked. A good constraint set carves a region that exists, has room, is weighted on purpose, and is cheap to sample.

RTL / Simulation Perspective — the failure modes in code

In code, the four failure modes are recognizable patterns. The example shows a contradiction (and the check), over-constraining, solver skew, and a performance note.

the four constraint-quality failure modes — and the cardinal randomize() check
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class txn extends uvm_sequence_item;
  rand bit [7:0] a, b;
  rand bit [6:0] size;
 
  // ✗ (1) CONTRADICTION → empty solution space → randomize() FAILS
  // constraint bad_c { size > 100; size < 50; }   // no value satisfies both → randomize returns 0
 
  // ✗ (2) OVER-CONSTRAINED → pinhole solution space → diversity collapses
  // constraint tight_c { size == 64; a == 0; b == 0; }  // only ONE solution → every txn identical
 
  // ✗ (3) UNINTENDED SKEW → solver does NOT sample uniformly over values
  constraint sum_c { a + b == 8'd200; }
  // looks "fair", but the solver skews toward certain (a,b) pairs → some a-values starve
  // FIX: weight explicitly where the coverage plan needs a spread:
  //   constraint a_dist_c { a dist { [0:50] := 1, [200:255] := 1, [51:199] := 3 }; }
 
  // ✗ (4) COMPLEXITY → slow solver (deep solve-before, nonlinear) → throughput craters at scale
  //   constraint slow_c { (a*b) % 7 == size[2:0]; solve a before b; }  // expensive every randomize()
endclass
 
// === THE CARDINAL CHECK: always verify randomize() succeeded ===
task body();
  txn t = txn::type_id::create("t");
  if (!t.randomize())                                   // ← check the RETURN, every time
    `uvm_fatal("RAND", "randomize() FAILED — contradictory constraints; stimulus would be STALE")
  start_item(t); finish_item(t);
endtask
// ✗ MISTAKE: `void'(t.randomize());` or `t.randomize();` with return ignored → silent stale stimulus

The code shows the four failure modes and the cardinal check. (1) Contradiction (size > 100 && size < 50) → empty solution space → randomize() returns 0 (fails). (2) Over-constrained (size==64; a==0; b==0) → one solution → every transaction identical (diversity collapse). (3) Unintended skew (a + b == 200) → looks fair, but the solver skews toward certain (a,b) pairs, so some a-values starvefixed by explicit weighting (a dist {...}) where the coverage plan needs a spread. (4) Complexity ((a*b) % 7 == size[2:0]; solve a before b) → expensive every randomize()throughput craters at scale. Then the cardinal check: if (!t.randomize()) uvm_fatal(...)check the return, every time — because the mistake (void'(t.randomize()) or ignoring the return) lets a contradiction drive silent stale stimulus. The shape to carry: the four failure modes are distinct, recognizable patterns — a contradiction (> and < that can't both hold), an over-constraint (equalities pinning every field), a skew (a structural constraint like a sum that the solver doesn't sample evenly), and a complexity cost (nonlinear, deep solve...before) — and the single most important habit is checking randomize()'s return, because that check turns a silent, stale-stimulus contradiction into a loud, immediate failure. The uvm_fatal on !randomize() is non-negotiable: without it, a bad constraint set fails into a frozen regression that looks like it's running. And the skew fix — explicit dist — encodes the truth that the solver won't give you a uniform spread for free. Check randomize() always; weight the distribution explicitly; keep the solution space roomy and the constraints cheap.

Verification Perspective — mapping each failure mode to its symptom

The skill is recognizing which quality failure you're seeing from its symptom — because the fixes differ. Seeing the four mapped symptom-to-cause is the diagnostic.

Four failure modes mapped to symptoms: contradiction, over-constraint, skew, slownessrandomize fails/ frozen stalelow diversity,repeatsa value starvesthroughput dropsContradictionempty spaceFails / frozen stalecheck randomize()Over-constrainedpinhole spaceLow diversity, repeatscoverage plateausUnintended skewclustered samplingA value starvesspread ≠ expectedComplexitygnarled spaceThroughput dropsslow per randomize12
Figure 2 — the four constraint-quality failure modes and their symptoms. Contradiction: randomize() fails (or, unchecked, stimulus is frozen at stale values) — the solution space is empty. Over-constrained: very low diversity, the same few transactions repeat, coverage plateaus far below target — the space is a pinhole. Unintended skew: a specific coverage value starves despite a legal path, the actual distribution differs from expectation — the solver clusters. Slow solver: simulation throughput drops, each randomize() takes long — the space is expensive to sample. Each symptom points at a different cause and a different fix.

The figure maps each failure mode to its symptom. Contradictionrandomize() fails (or, unchecked, stimulus is frozen at stale values) — the solution space is empty; the fix is check randomize() and resolve the conflict. Over-constrainedvery low diversity, the same few transactions repeat, coverage plateaus far below target — the space is a pinhole; the fix is loosen the constraints. Unintended skew → a specific coverage value starves despite a legal path, the actual distribution differs from expectation — the solver clusters; the fix is explicit dist and verify the spread. Slow solversimulation throughput drops, each randomize() takes long — the space is expensive to sample; the fix is simplify the constraints. The verification insight is that these symptoms are distinct and point at different causes and fixes — so misreading the symptom wastes effort. Frozen/stale stimulus or randomize() warnings → contradiction (check the return, fix the conflict) — not a stimulus gap. Low diversity / plateaued coverageover-constraint (loosen) — not "need more seeds" (more seeds of a pinhole give the same few transactions). A starved value with a legal pathskew (explicit dist) — not a missing constraint path (the path exists, it's just rarely sampled). Slow simscomplexity (simplify) — not a machine problem. The warning-colored failure modes map to the brand-colored symptoms, and recognizing the symptom names the cause. This is why constraint quality is a diagnostic skill: the same high-level complaint ("coverage isn't closing") can stem from a contradiction (frozen), an over-constraint (repeats), or a skew (starved value) — and only the specific symptom distinguishes them. The figure is the symptom map: contradiction (fails/frozen), over-constraint (low diversity), skew (starved value), slowness (low throughput) — four failure modes, four symptoms, four fixes. Read the symptom to name the constraint-quality failure: frozen, repeating, starved, or slow.

Runtime / Execution Flow — what happens when randomize() fails

At run time, a contradiction produces a specific, dangerous sequence — and whether you check the return determines whether it's a loud failure or a silent stale-stimulus bug. The flow shows both paths.

randomize() failure: returns 0, fields unchanged; checked path fatal, unchecked path stalerandomize() → contradictory constraints → no solution, returns 0, fields unchanged → checked: fatal (loud) / unchecked: stale stimulus (silent)randomize() → contradictory constraints → no solution, returns 0, fields unchanged → checked: fatal (loud) / unchecked: stale stimulus (silent)1randomize() is calledthe solver attempts to satisfy all active constraints (legality +steering).2No solution → returns 0contradictory constraints leave the space empty; randomize()returns 0 and leaves the fields unchanged.3Checked → fatal (loud)if (!t.randomize()) uvm_fatal — the failure is raised immediatelyand traceably.4Unchecked → stale stimulus (silent)the object keeps its old/default values; the testbench drives afrozen transaction — coverage stalls, no error.
Figure 3 — the two paths after randomize() encounters a contradiction. The solver tries to find a solution to the active constraints. If they're contradictory, no solution exists, and randomize() returns 0 without changing the object's fields. From here, two paths: if the return is checked, the testbench raises a fatal error immediately — a loud, traceable failure. If the return is unchecked, the object keeps its previous or default values, and the testbench drives that stale transaction as if it were freshly randomized — a silent bug where the stimulus is frozen and coverage stalls with no error. Checking the return is what makes the failure visible.

The flow shows the two paths after a contradiction. Call (step 1): randomize() attempts to satisfy all active constraints (legality + steering). Fail (step 2): contradictory constraints leave the space empty; randomize() returns 0 and leaves the fields unchanged. From here, two paths. Checked (step 3): if (!t.randomize()) uvm_fatal(...) — the failure is raised immediately and traceably (loud). Unchecked (step 4): the object keeps its old/default values; the testbench drives a frozen transaction — coverage stalls, no error (silent). The runtime insight is the critical detail that randomize() leaves the fields unchanged on failure — it does not zero them or flag them; it returns 0 and the object retains whatever it had. So an unchecked failure is invisible at the point of failure: the testbench proceeds with a transaction that looks valid (it has values — just stale ones) and drives it. The consequence unfolds downstream: if every randomize() fails, every transaction is the same stale one, so the regression runs but exercises nothingcoverage flatlines with no error message. This is why the check is cardinal: it's the only thing that converts the silent failure (step 4) into the loud one (step 3). The check costs one line (if (!randomize()) uvm_fatal) and saves days of debugging a frozen regression. The flow is the fork: randomize() fails → returns 0, fields unchanged → checked (fatal, loud) or unchecked (stale, silent) — and the verifier's discipline is to always take the checked path. The runtime moral is that the solver's failure mode is quiet by default — it returns a status rather than throwing — so the burden is on you to check it. randomize() fails quietly and leaves stale values — check its return, or a contradiction becomes an invisible frozen-stimulus bug.

Waveform Perspective — frozen stimulus from an unchecked failure

The symptom of an unchecked contradiction is visible on a timeline: a healthy stream varies every transaction, while a failing-and-unchecked stream is frozen — the same stale values, transaction after transaction. The waveform shows both.

A checked randomize() varies the stimulus; an unchecked failure freezes it at stale values

12 cycles
A checked randomize() varies the stimulus; an unchecked failure freezes it at stale valueshealthy: randomize() succeeds → size_good varies (12, 48, 31...) → coverage moveshealthy: randomize() s…failing: contradictory constraints → randomize() returns 0 → size_stale unchangedfailing: contradictory…size_stale frozen at default (00) every txn — the field never changessize_stale frozen at d…cov_frozen stays 0 — coverage stalls, but NO error (return unchecked)cov_frozen stays 0 — c…clkrand_oksize_good1212484831315959774040size_stale000000000000000000000000cov_movingcov_frozent0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — frozen stimulus from an unchecked randomize() failure. The healthy stream (rand_ok high) randomizes successfully each transaction, so its size field varies. The failing stream has contradictory constraints, so randomize() returns 0 every time (rand_ok low) and leaves size unchanged — the field is frozen at its stale default (size_stale constant) across every transaction. With the return unchecked, the testbench drives this frozen value as if it were fresh, so coverage stalls with no error. The constant field across transactions is the visible signature of an unchecked randomization failure.

The waveform shows frozen stimulus from an unchecked failure. The healthy stream (rand_ok high) randomizes successfully each transaction, so its size field varies (size_good: 12, 48, 31, 59...). The failing stream has contradictory constraints, so randomize() returns 0 every time (rand_ok low) and leaves size unchanged — the field is frozen at its stale default (size_stale constant at 00) across every transaction. The crucial reading is the contrast in the field's behavior: a healthy randomization varies the field (different value each transaction → coverage moves, cov_moving), while an unchecked failure freezes it (same value every transaction → coverage stalls, cov_frozen stays 0). With the return unchecked, the testbench drives this frozen value as if it were fresh — so the regression runs (transactions issue, the protocol proceeds), but it exercises nothing new (the same stale transaction, repeatedly), and coverage stalls with no error. The constant field across transactions is the visible signature of an unchecked randomization failure — and it's exactly the kind of symptom you'd see in a waveform or a transaction log when debugging a flatlined coverage curve. The picture to carry is that a frozen field (the same value, transaction after transaction, when it should vary) is the fingerprint of a failed-and-unchecked randomize()not a DUT issue, not a coverage issue, but a constraint-quality (contradiction) issue masquerading as running stimulus. Reading the waveform this way — is the random field actually varying, or frozen? — is catching an unchecked failure. The frozen size_stale against the varying size_good is the signature of the silent contradiction: the stimulus stopped randomizing and nobody was told. A random field that never changes is a randomize() that failed unchecked — check the return, and watch for frozen stimulus.

DebugLab — the regression that ran for a week on one transaction

Coverage flatlined while the regression 'ran' — a contradiction with an unchecked randomize()

Symptom

A team added a new constraint to a sequence to target a corner — len inside {[60:64]} for a boundary test, layered onto a base that already had len < 32 from an earlier throughput-focused scenario. The regression launched normally, ran for a week, all tests passing, no errors. But coverage flatlined — it stopped moving the moment the new constraint went in, sitting at the same number day after day. Inspecting the transaction log, every single transaction in the boundary test was identical: the same len, the same addr, the same everything — a frozen stimulus stream. The regression had run for a week and exercised exactly one transaction, millions of times, with no indication anything was wrong.

Root cause

The new constraint contradicted the inherited base constraint — len inside {[60:64]} and len < 32 have no common solution — so randomize() failed and returned 0; but the return was unchecked, so the object kept its stale default values and the testbench drove them forever:

why a week-long regression exercised one frozen transaction
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
✗ CONTRADICTORY constraints + UNCHECKED randomize():
  // base (inherited):  constraint base_c { len < 32; }          // from an earlier scenario
  // test (new):        constraint bnd_c  { len inside {[60:64]}; }  // boundary target
  // BOTH active → len < 32 AND len >= 60 → NO common solution → randomize() returns 0
  seq_item.randomize();          // ← return IGNORED → object keeps STALE default (len=0, addr=0...)
  // every txn is the identical stale item → coverage flatlines → regression "runs" for a week, exercises 1 txn
 
✓ CHECK the return (catch it instantly) AND resolve the contradiction:
  if (!seq_item.randomize())
    `uvm_fatal("RAND","randomize() failed — len constraints contradict (len<32 vs len in [60:64])")
  // → fails on the FIRST transaction with a clear message, not silently for a week
  // resolve: the base's len<32 was leftover STEERING in the wrong place (Module 20.2) — remove it,
  //   or override it in the boundary test so the [60:64] target is reachable

This is the unchecked-contradiction bug — the cardinal constraint-quality failure. The team layered a boundary constraint (len inside {[60:64]}) onto a base that already carried len < 32 (leftover steering from an earlier scenario — itself a Module 20.2 layering smell). The two are contradictoryno len is both < 32 and >= 60 — so randomize() returned 0. But the call was seq_item.randomize(); with the return ignored, so the object kept its stale default (len=0, addr=0, ...) and the testbench drove that frozen itemevery transaction identical, coverage flatlined, and the regression "ran" for a week while exercising exactly one transaction. The silence is the damage: no error, all tests "passing" (they issue transactions and complete), coverage simply not moving — a failure mode that looks like progress. The fix is two-fold. First, the cardinal check: if (!seq_item.randomize()) uvm_fatal(...) — which would have caught the contradiction on the first transaction with a clear message ("len constraints contradict"), not silently for a week. Second, resolve the contradiction: the base's len < 32 was leftover steering in the wrong place (the Module 20.2 lesson — steering baked into a shared base), so remove it or override it in the boundary test so the [60:64] target is reachable. The general lesson, and the chapter's thesis: always check randomize()'s returncontradictory constraints make randomize() fail and return 0, and because it leaves the object's fields unchanged, an unchecked failure silently drives stale, frozen stimulus that flatlines coverage with no error, so a regression can "run" for a week and exercise one transaction; check the return (if (!randomize()) uvm_fatal) to convert the silent failure into a loud, immediate one, and design constraints to be consistentespecially watching for contradictions introduced by layering a test constraint onto an inherited base. A coverage curve that flatlines the instant a constraint changes, with the regression still "running," is an unchecked randomize() failure — check the return, and a week of wasted compute becomes a first-transaction fatal.

Diagnosis

The tell is coverage that flatlines while the regression keeps running with no errors. Diagnose an unchecked contradiction:

  1. Check whether randomize() returns are checked. An ignored randomize() return is the precondition for a silent contradiction; grep for unchecked calls.
  2. Look for frozen fields in the transaction log. Random fields that never change across transactions are the signature of a failed-and-unchecked randomize().
  3. Inspect recently-added or layered constraints. A new test constraint contradicting an inherited base constraint is the common trigger; check the combined active set.
  4. Correlate the flatline with a constraint change. Coverage stopping the instant a constraint went in points at that constraint creating an empty solution space.
Prevention

Check the return and keep the space healthy:

  1. Always check randomize() success. Wrap every call: if (!obj.randomize()) uvm_fatal — make a silent failure impossible.
  2. Resolve contradictions, and watch layering. A test constraint contradicting an inherited base is the common cause; keep steering out of shared bases (Module 20.2).
  3. Keep the solution space roomy. Avoid over-constraining; constrain only legality and intended steering so diversity survives.
  4. Verify the actual distribution. Don't assume uniformity; check coverage for starved values and weight with dist where the solver skews.

The one-sentence lesson: always check randomize()'s return — contradictory constraints make it fail and return 0, and because it leaves the object's fields unchanged, an unchecked failure silently drives stale, frozen stimulus that flatlines coverage with no error, so a regression can run for a week and exercise one transaction; check the return to make the failure loud and immediate, and design constraints to be consistent, watching for contradictions introduced by layering.

Common Mistakes

  • Ignoring randomize()'s return. An unchecked failure drives stale, frozen stimulus silently; always wrap in if (!randomize()) uvm_fatal.
  • Layering a contradictory constraint. A test constraint conflicting with an inherited base empties the solution space; check the combined active set and keep steering out of shared bases.
  • Over-constraining into a pinhole. Too many constraints leave a few solutions that repeat; coverage plateaus — loosen rather than adding seeds.
  • Assuming rand is uniform. The solver guarantees a legal solution, not an even spread; verify the distribution and weight with dist where a value starves.
  • Letting constraints get needlessly complex. Nonlinear and deeply-ordered constraints slow every randomize(); simplify to protect simulation throughput.
  • Misreading the symptom. Frozen (contradiction), repeating (over-constraint), starved value (skew), and slow (complexity) need different fixes — diagnose before acting.

Senior Design Review Notes

Interview Insights

A good constraint set defines a healthy solution space — the region of legal transactions the solver samples — across four properties: it's non-empty, large enough, correctly weighted, and cheap to sample. Think of the constraints as carving a region in the space of all possible transactions, and the solver dropping a dart into that region each randomize. Quality is the health of that region. First, non-empty, meaning solvable: the constraints must be consistent, because contradictory constraints carve an empty region where the dart has nowhere to land, so randomize fails and returns zero. Second, large enough, meaning not over-constrained: too many constraints shrink the region to a pinhole, so every dart lands in nearly the same spot, diversity collapses, and the engine repeats a handful of transactions. Third, correctly weighted, meaning no unintended skew: the dart isn't guaranteed to land uniformly — the solver finds a legal solution but doesn't promise an even spread, so constraint structure like sums or implications can pull the sampling toward some areas and starve a value the coverage plan needs, unless you weight deliberately with dist. Fourth, cheap to sample, meaning performant: a gnarled, complex region with nonlinear or deeply-ordered constraints is slow for the solver to find a point in, taxing every randomize call and cratering simulation throughput at scale. The crucial thing is that all four are independent and none is checked by the compiler — a constraint set can elaborate cleanly and still be empty, or a pinhole, or skewed, or slow. They surface only as runtime symptoms: frozen or failing randomize, low diversity, starved bins, or slow sims. So constraint quality is a distinct discipline beyond getting the intent right: you check randomize's return, keep the space roomy, control the distribution explicitly, and keep constraints cheap. The mental model is asking whether the carved region exists, has room, is covered as intended, and is easy to land a dart in — and checking that the dart actually landed.

Because randomize returns a status — zero on failure — and on failure it leaves the object's fields unchanged, so an unchecked failure silently drives stale, frozen stimulus with no error. When the constraints are contradictory, the solution space is empty, and the solver can't find any assignment satisfying them. SystemVerilog handles this by having randomize return zero rather than throwing — and critically, it does not zero or flag the fields; it leaves them at whatever they were, the previous or default values. So if you call randomize and ignore the return, the object still has values — just stale ones — and your testbench proceeds to drive that transaction as if it were freshly randomized. The failure is invisible at the point it happens. Downstream, the consequence is severe: if every randomize fails, every transaction is the same stale one, so the regression runs, transactions issue, the protocol proceeds, tests pass — but coverage flatlines because you're exercising one frozen transaction over and over. There's no error message; it just looks like coverage stopped moving. The classic disaster is a regression running for a week, all green, that turns out to have exercised exactly one transaction millions of times, because a layered constraint contradicted an inherited base and nobody checked the return. The fix is one line: if not obj dot randomize, then uvm_fatal. That converts the silent failure into a loud, immediate one — it fails on the very first transaction with a clear message instead of silently for a week. This is why checking the return is cardinal and non-negotiable: it's the only thing that makes a contradiction visible, and it costs one line to save days of debugging a frozen regression. The signature to watch for, if you didn't check, is a random field that never changes across transactions — a frozen field is the fingerprint of a failed-and-unchecked randomize. So the discipline is to wrap every randomize call in a return check, every time, no exceptions.

The solver guarantees a legal solution to the constraints, not a uniform spread over the values, because it solves for satisfaction, and the structure of the constraints biases which solutions get picked — so you control the distribution explicitly with dist and verify it with coverage. The common misconception is that a plain rand variable comes out uniformly distributed. It doesn't, in general. The solver's job is to find an assignment that satisfies all the constraints; how it picks among the many legal assignments isn't guaranteed to be uniform over any particular variable's values. Constraint structure skews it. A simple example: if you constrain a plus b equals a constant with both rand, the solver doesn't produce uniform a — certain a-values, and certain a-b pairings, get favored by how the solver explores, so some a-values appear far more than others. Implication constraints, where one variable's range depends on another, skew similarly. Even without explicit interaction, the solver's internal method and any solve-before ordering affect the spread. The practical consequence is that a constraint that looks fair can starve a coverage value: you expect a particular value to show up regularly, but it appears rarely because the solver's sampling clusters elsewhere, and the corresponding coverage bin stays under-hit despite a legal path existing. What you do about it is two things. First, don't assume — verify the actual distribution by looking at coverage; if a value with a legal path is starving, suspect skew. Second, control it explicitly: use dist to assign weights where the spread matters, so instead of relying on the solver's incidental distribution, you specify that this range should occur with this weight. dist gives you direct control over the probability mass, which is exactly the aiming mechanism from randomization planning. So the rule is: the solver promises legality, not uniformity; never rely on an assumed even spread for coverage-critical values; weight them with dist and confirm with coverage. This is distinct from over-constraining or contradiction — the path exists and the space is healthy, it's just sampled unevenly, so the fix is weighting, not loosening.

An over-constrained set has a tiny but non-empty solution space, so randomize succeeds but produces almost no diversity; a contradictory set has an empty solution space, so randomize fails entirely. They're different points on the same axis of too-tight, with different symptoms and fixes. Over-constraining means you've added so many constraints that very few legal solutions remain — maybe a handful, maybe one. randomize still succeeds, because solutions exist, but it returns the same small set of transactions repeatedly. The symptom is low diversity: the transaction log shows the same few patterns over and over, and coverage plateaus far below target because most bins never get hit — you're exercising a pinhole of the space. The trap is mistaking this for a stimulus gap and adding more seeds, but more seeds of a pinhole give the same few transactions; the fix is to loosen the constraints so the space has room. Contradiction is the extreme: the constraints have no common solution at all, so the space is empty. randomize fails and returns zero, leaving the fields stale. If checked, you get a fatal error; if unchecked, you get frozen stimulus — the same stale transaction every time, coverage flatlined, no error. So the symptoms differ: over-constraint shows low-but-nonzero diversity with randomize succeeding, while contradiction shows zero diversity with randomize failing. Diagnostically, the key signal is the randomize return — succeeding but repetitive points to over-constraint, failing points to contradiction. And the field behavior differs: over-constraint produces a few different values cycling, contradiction produces one frozen value. The fixes share a direction — relax the constraints — but contradiction requires finding the specific conflict, often a layered test constraint versus an inherited base, while over-constraint requires identifying which of the legitimate-looking constraints collectively starve the space. Both are invisible to the compiler and both surface only as coverage symptoms, which is why recognizing the difference — repeats versus frozen, succeeds versus fails — is the diagnostic skill.

Constraints affect performance because the solver runs on every randomize call, and complex constraint sets make each solve slow, so at high transaction volumes a slow solver craters simulation throughput — you keep them efficient by minimizing complexity and using solver hints judiciously. Every time you randomize an object, the constraint solver has to find a legal assignment satisfying all active constraints. For simple constraints — ranges, a few inequalities — this is fast. But complexity makes it expensive: large numbers of interacting variables, nonlinear constraints like multiplication or modulo, deep chains of solve-before ordering, and constraints that couple many variables together all force the solver to do more work to find each solution. Individually a slow solve might be milliseconds, but a constrained-random regression calls randomize millions of times, so a solver that's even modestly slow per call multiplies into a major throughput loss — the simulation spends its time solving constraints instead of simulating the DUT. The symptom is dropping throughput: cycles per second fall, the regression takes much longer, and profiling shows time in the constraint solver. To keep constraints efficient: first, minimize unnecessary complexity — express constraints in the simplest form that captures the legality and steering you need, and avoid gratuitous nonlinear or deeply-coupled relationships. Second, use solve-before deliberately rather than by default — it controls ordering and can help or hurt, so apply it where it genuinely improves the distribution or performance, not reflexively. Third, scope constraints appropriately — don't impose expensive constraints on variables that don't need them. Fourth, consider whether some complex relationship can be computed post-randomize instead of constrained, when correctness allows. And throughout, be aware that constraint complexity is a runtime tax, so it's a real engineering trade-off, especially for high-volume transaction types. The principle is that constraints aren't free — they run at simulation time, on every randomize — so for the hot paths you keep the carved region cheap to sample, balancing the expressiveness you need against the solver cost you pay millions of times.

Exercises

  1. Spot the contradiction. Given a base with len < 32 and a test adding len in [60:64], explain what randomize() returns and what the testbench drives if unchecked.
  2. Diagnose by symptom. For frozen stimulus, repeating transactions, a starved value, and slow sims, name the constraint-quality failure and the fix.
  3. Fix the skew. A value with a legal path is under-hit. Explain why the solver might starve it and how dist resolves it.
  4. Write the check. Write the randomize() call with its return check, and explain what it converts a contradiction into.

Summary

  • Constraint quality is whether the constraints define a healthy solution space across four properties: non-empty (solvable), large enough (not over-constrained), correctly weighted (no unintended skew), and cheap to sample (performant) — none checked by the compiler.
  • Always check randomize()'s return: contradictory constraints make it fail (return 0) and leave the fields unchanged, so an unchecked failure silently drives stale, frozen stimulus that flatlines coverageif (!obj.randomize()) uvm_fatal converts the silent failure into a loud one.
  • The solver does not guarantee uniformity: it finds a legal solution, and constraint structure (sums, implications, ordering) skews the spread, so a value with a legal path can starvecontrol the distribution explicitly with dist and verify it.
  • The failure modes map to distinct symptoms: contradiction → fails/frozen; over-constraint → low diversity/repeats; skew → a starved value; complexity → low throughput — each a different fix (check+resolve, loosen, weight, simplify).
  • The durable rule of thumb: constraints carve a region and the solver drops a dart in it — make the region exist (check randomize(), resolve contradictions), give it room (don't over-constrain), cover it as intended (the solver isn't uniform, so weight with dist and verify), and keep it cheap (simplify for throughput); a coverage curve that flatlines while the regression still 'runs' is an unchecked randomize() failure, so check the return every time.

Next — Coverage Feedback: the loop that ties stimulus to coverage — how the coverage report drives the next randomization. Reading the gaps, deciding whether to bias constraints, add a directed test, or adjust the coverage model, and running the iterative feedback loop — generate, measure, analyze, steer, repeat — that converges constrained-random verification toward closure.