|

Why SystemVerilog Randomization Fails (and How to Debug It)

In short: randomize() fails when no set of values can satisfy every active constraint at once. It returns 0, the solver prints the constraints that clash, and the object keeps its old values. Almost every failure comes from one of six causes: an inline with that fights a class constraint, a child class that fights its parent, a non-rand variable nobody set, a rand handle that was never constructed, a partial randomize(x) that freezes everything else, or a return value that was ignored. The last one is the worst: the error scrolls past in the log, the test keeps going, and it quietly sends old items again. This guide shows all six on one small class with real VCS logs.

Constraint failures come up again and again on the Verification Academy and Cadence forums (threads linked in the references). They are also easy to fix once you can read what the solver is telling you.

The analogy: an impossible food order

You order “a vegetarian burger with extra chicken”. The kitchen can’t make it, so it sends nothing back. Randomization works the same way:

  • The menu rules are the class constraints.
  • Your special request is the inline with { }.
  • The kitchen is the constraint solver.
  • “Sorry, we can’t make that” is randomize() returning 0, plus the solver’s message listing the clashing rules.

The dangerous case is a waiter who doesn’t tell you. You assume the new burger arrived, but you’re eating yesterday’s order again. That’s what happens when you ignore the return value.

What actually happens when randomize() fails

Three things happen, and you need all three to debug well:

  1. It returns 0. randomize() is a function that returns 1 on success and 0 on failure. That return value is the only reliable signal in your own code.
  2. The rand variables keep their old values. Nothing is assigned on failure. A fresh object keeps its defaults, often 0, and an object you reuse keeps the last good values.
  3. The simulator prints the clash. Each tool lists the constraints it couldn’t satisfy together:
SimulatorWhat it printsExtra help
Synopsys VCSError-[CNST-CIF] Constraints inconsistency failure, then “Solver failed when solving following set of constraints”Run options +ntb_solver_debug=trace +ntb_solver_debug_filter=<serial>; Verdi constraint debug
Cadence Xcelium*W,SVRNDF “The randomize method call failed”, then RNDOCS lines listing the conflicting constraintsSimVision constraint debugger
Siemens QuestaA randomization-failed messagevsim -solvefaildebug

The VCS messages and switches above are quoted from a real log posted on Verification Academy. The Xcelium ones come from the Cadence forum, and the Questa switch from this thread. The demo below shows the exact VCS output.

The assert() trap

// Don't: if assertions are switched off, randomize() may never be called
assert(req.randomize());

// Do: check the return value and say what you asked for
if (!req.randomize() with { burst == WRAP; len == 7; })
  `uvm_error("RAND", $sformatf("randomize failed: %s", req.convert2string()))

Many teams switch off immediate assertions in fast regressions. When they do, the randomize() call inside assert() can disappear with them, because the tool may skip evaluating the whole expression. Every item then goes out unrandomized, and nothing complains. The Questa thread above gives the same advice: never wrap randomize() in assert().

The six causes of a randomization failure

Six causes of SystemVerilog randomization failure with the VCS log clue for each: inline with conflict, child vs parent constraint, unset state variable, unconstructed rand handle, randomize with an argument list, unchecked return value
Each cause was planted in one small class and run on VCS. The clue is the line to look for in the log.
#CauseFood-order versionClueFix
1Inline with fights a class constraintThe menu says wraps come in 4 sizes, and you ask for a fifthFails every time, and only at that call siteRead the class constraints the solver lists. Change the request, or switch a constraint off on purpose with constraint_mode(0)
2Child class fights its parentA new branch adds a rule the head office forbidsEvery randomize of the child fails, while the parent is fineOverride the parent constraint by giving it the same name in the child, instead of adding a second one
3A non-rand variable nobody setThe order form has “max size” left blank, so it reads as zeroConstraints look right, but one side compares against 0Set state variables in new() or before randomize(); print them in the error message
4A rand handle that was never constructedOrdering a side dish for a table that doesn’t existA null-object error, or a sub-object that never changes, depending on the toolrand doesn’t create objects. Call new() first
5randomize(x) freezes everything elseChanging only the drink on a finished order, when the new drink doesn’t fit the trayFails only for some seedsPass every variable that must move: randomize(len, addr)
6The return value is ignoredThe waiter says nothing and serves yesterday’s orderThe test keeps passing. Items repeat, or coverage stallsAlways check the return value and stop or report on 0

Two extra checks that catch subtle failures:

  • Width and sign. A constraint like addr[11:0] + bytes <= 4096 is evaluated at the width of its operands. If a term is too narrow, it can wrap and make a legal value look illegal, or the other way round. Write the widths out (32'd1) in boundary maths.
  • soft versus hard. A soft constraint quietly gives way when a hard one disagrees. If two of your intentions must both hold, neither can be soft.

A 5-minute triage

Triage flow when randomize returns 0: null handle, fixed non-rand value, two clashing rules, or repeating items, each with its fix
Read the first solver block, then follow the branch it points to.
  1. Find the first failure, with its seed. Note the seed, the call site and what you asked for in the with.
  2. Does it fail every time, or only sometimes? Every time: two constraints simply can’t both be true (causes 1 to 3). Go to step 3. Only for some seeds: something was frozen or chosen earlier (cause 5). Look for randomize(x) with an argument list, two-step randomization, or values set in pre_randomize().
  3. Read the solver’s list, not your code. VCS, Xcelium and Questa all print the clashing constraints. Cross out every line that can’t involve your variable. The two or three left are the fight.
  4. Print the non-rand inputs. State variables, loop bounds and config values feed the solver as fixed numbers. One of them being 0 is cause 3.
  5. Shrink it. Switch off constraints one at a time with constraint_mode(0) until it passes. The last one you switched off is half of the clash.
  • Analogy: when an order can’t be made, don’t rewrite the whole menu. Ask the kitchen which two rules clash, then change one.
  • If nothing failed but the coverage is stuck, suspect cause 6: a randomize somewhere fails with its return value unchecked, and its items are repeats.

The practical verification debug guide uses the same “first failure first” habit for every kind of bug.

One class, six planted failures, real VCS logs

The demo is plain SystemVerilog, with no UVM needed. It uses one AXI-style burst request class:

  • Constraints: 32-bit bus sizes, beat alignment, legal wrap lengths (2, 4, 8 or 16 beats), and the 4KB boundary rule for INCR bursts.
  • A rand sub-object for priority.
  • A child class for a 64-bit bus variant.

+BUG=N switches on one cause at a time. The run goes through four phases and prints one summary line for each:

PhaseWhat it doesBugs it shows
P1200 requests with a test-level with1, 3, 4
P250 requests from the 64-bit child class2
P3Pick a burst, then make it longer in a second step5
P4100 wrap bursts, checked for repeats6

Run it yourself

The whole demo is one file, rand_fail_demo.sv, with all six planted causes.

vcs -full64 -sverilog rand_fail_demo.sv -R +BUG=0

Then +BUG=1 to +BUG=6. On EDA Playground, put +BUG=N in Run Options. If it does not reach the run, compile with +define+BUG=N instead. The first line of every log prints the planted bug, so you can check.

Try the triage before reading the results below: run one bug, read only the first solver block, and name the cause.

What VCS printed

Every run below is the same file with a different +BUG=N. Each phase stops after 5 failures so the log stays readable (VCS prints a full solver report for every failed call).

RunWhat brokeP1P2P3P4
BUG=0nothing200 ok50 ok200 ok100 new, 0 repeated
BUG=1inline with vs class rule5 of 5 failed50 ok200 ok100 new
BUG=2child vs parent200 ok5 of 5 failed200 ok100 new
BUG=3max_len never set5 of 5 failed50 ok5 of 5 failed5 sent, 4 repeated
BUG=4user never constructed5 of 5 failed5 of 5 failed5 of 5 failed5 sent, 4 repeated
BUG=5randomize(len) alone200 ok50 ok196 ok, 4 failed100 new
BUG=6return value ignored200 ok50 ok200 ok100 sent, 50 repeated

What the logs tell you, run by run:

  • BUG=1. VCS prints Error-[CNST-CIF] Constraints inconsistency failure and lists exactly two rules: c_wrap (len inside {1, 3, 7, 15}) and WITH_CONSTRAINT (burst == WRAP; len == 8'h14). That’s 20, which a wrap burst can’t have. The object still holds its old values: addr=00000000 len=0 size=0 burst=FIXED.
  • BUG=2. The list shows bit[0:0] wide64 = 1'h1 (not rand, so fixed), then c_size from the parent (size <= 2) and c_wide from the child (size == 3). Two files, one clash.
  • BUG=3. The list shows bit[7:0] max_len = 8'h0. No rand in front, and the value is 0. That one line is the whole bug. It fails again in P3 and P4 with different inline rules, because every call hits the same frozen 0.
  • BUG=4. This one is a different error: Warning-[CNST-PPRW] Member object 'user' was found to be NULL during randomization, then Error-[CNST-NPE] Accessing null pointer user.prio in constraints. VCS does flag it. The AMIQ post warns that not every simulator does, so don’t count on it.
  • BUG=5. 4 failures in 200, the first at request 6. The list shows addr = 32'hfff4 as a fixed value, not rand. The offset inside the 4KB page is 0xff4, which leaves 12 bytes, room for 3 beats of 4 bytes. len >= 32 can’t fit. On most seeds the first step picked an address with room, so the bug hid.
  • BUG=6. VCS still printed the solver error each time, but the test didn’t check the result, so it kept going. Every illegal request sent the previous burst again: 50 repeats in 100. Nothing in the test’s own summary says a single call failed.

The pattern is worth remembering. In five of the six runs, the answer was sitting in the first solver block. You only had to read the fixed values (the lines without rand) and the two rules VCS named.

Debug techniques that work on any simulator

These are short patterns. The runnable, tested code is the demo file.

1. Switch constraints off one at a time

req.c_4k.constraint_mode(0);          // switch one constraint block off
if (req.randomize() with { len == 20; }) $display("c_4k was in the clash");
req.c_4k.constraint_mode(1);          // always switch it back on
  • constraint_mode(0) on a named block is the fastest way to find which block is in the fight. That’s why every constraint block should have a clear name.
  • Analogy: taking one rule off the menu at a time until the kitchen says “yes, we can make that”.

2. Freeze or free variables on purpose

// Option A: freeze one variable, solve the rest
req.addr.rand_mode(0);                // addr keeps its current value
ok = req.randomize();
req.addr.rand_mode(1);

// Option B: solve only the named variables, everything else stays fixed
ok = req.randomize(len, addr);
  • rand_mode(0) turns a rand variable into a fixed input.
  • An argument list on randomize() solves only the named variables. That is what cause 5 gets wrong.

3. Make the error message do the triage

if (!req.randomize() with { burst == WRAP; len == want; })
  `uvm_error("RAND", $sformatf("failed: asked burst=WRAP len=%0d, max_len=%0d, obj=%s",
                                want, req.max_len, req.convert2string()))

Print what you asked for and every non-rand input. That answers triage step 4 without rerunning.

4. Use soft constraints for defaults only

constraint c_len_default { soft len inside {[0:15]}; }   // a default the test can override

A test’s inline with { len == 100; } beats a soft default instead of failing. Use soft for defaults that tests are allowed to override, and never for a protocol rule.

5. Use the simulator’s own debugger when the list is long

  • VCS: run with +ntb_solver_debug=trace +ntb_solver_debug_filter=<serial>, or step the solver in Verdi.
  • Xcelium: the SimVision constraint debugger.
  • Questa: vsim -solvefaildebug.

All three are taken from the forum threads in the references. Check your own tool’s manual for the current spelling.

Common mistakes, from real forum threads

1. “My constraint is ignored” on a variable that isn’t rand

  • What happens: the variable in the constraint was never declared rand, so the solver treats it as a fixed number. A reply on Verification Academy puts it simply: constraints do not work on non-rand values.
  • Fix 1: declare it rand if it should be chosen.
  • Fix 2: if it’s a setting, give it a value before randomize() and print it in the error.

2. Calling randomize() on a null handle

  • What happens: “Invalid call to class::randomize() via null class reference”. randomize() doesn’t create objects (thread).
  • Fix: call new() first, including for every rand sub-object handle. AMIQ’s gotcha post notes that some simulators give no error at all for an uninitialised rand handle.

3. A dist that conflicts with a hard constraint

  • What happens: a dist adds a set of allowed values, not just weights. If a hard constraint rules out every value in the set, randomization fails. The Cadence thread is exactly this case.
  • Fix 1: make sure each dist range overlaps what the other constraints allow.
  • Fix 2: if the weights are only a preference, consider soft for that rule.

You can see this exact dist versus hard constraint clash, and the constraint_mode(0) fix, in the write sequence of async FIFO verification part 1.

4. A constraint that “sometimes” fails

  • What happens: it passes on most seeds and fails on a few. Usually something is frozen, or an indexed or modulo expression narrows the space more than expected (thread with a VCS log).
  • Fix 1: rerun the failing seed and read the solver list.
  • Fix 2: simplify the expression. In that thread, rewriting as a foreach solved it.

5. Wrapping randomize() in assert()

  • What happens: the call can vanish when assertions are off (thread).
  • Fix: use if (!obj.randomize()) ..., as in the assert() trap above.

6. Randomizing a wrapper class and expecting the inner objects to change

  • What happens: the outer object randomizes, but the inner object’s fields don’t, because the handle isn’t rand, or isn’t constructed.
  • Fix 1: mark the handle rand.
  • Fix 2: construct it in new().
  • Fix 3: check one inner value in a quick test.

How AI helps (and where it doesn’t)

  • Reading the solver list. Paste the solver’s conflict block and the class’s constraints, and ask which two clash and why. An AI is quick at this, and it’s a good first pass.
  • Proposing a minimal fix. Ask for the smallest change that keeps every protocol rule. Then check it: an AI will happily “fix” a failure by deleting the very rule that protects the protocol.
  • Where it doesn’t help: deciding which rule is right. If the 4KB rule and the test’s request clash, only the spec decides which one gives way.

More in AI in ASIC verification, and a real experiment in AI-written vs hand-written UVM testbench.

FAQ

Why does randomize() return 0 instead of stopping the simulation? The standard makes randomize() a function that returns 1 on success and 0 on failure. Deciding what a failure means is your job, which is why every call needs a check.

What values does the object hold after a failed randomize()? The random variables keep the values they had before the call. That’s why an unchecked failure looks like a repeated transaction (planted bug 6 in the demo).

Can a constraint fail only on some seeds? Yes. If part of the object is frozen (a non-rand variable, or a variable left out of a randomize(arg) list), some earlier values leave no room for the new rule. Planted bug 5 shows this.

Does a soft constraint ever cause a failure? No. When a soft constraint conflicts with a hard one, the solver drops the soft one. That’s also its risk: it can be dropped without a message.

Is it safe to switch off a constraint to make a failure go away? Only to find the culprit. Once you know which rule clashes, fix the cause, or you may be sending illegal traffic.

Do I need UVM for any of this? No. The demo is plain SystemVerilog. In UVM, the same rules apply to seq_item.randomize() and the uvm_do_with macro.

References

Read next

Leave a Reply

Your email address will not be published. Required fields are marked *