Distribution bugs never fail a test. Every call returns 1 with legal values, while a corner case appears once per million transactions instead of once per fifty.
The only witness is measurement. Bump counters in post_randomize over a short standalone loop, and keep a covergroup on rand fields so every regression watches the spread.
class txn;
rand bit is_err;
rand bit [3:0] len;
constraint err_c { is_err dist { 1 := 1, 0 := 9 }; } // intend 10% errors
static int unsigned hist_err[2];
static int unsigned hist_len[16];
function void post_randomize();
hist_err[is_err]++;
hist_len[len]++;
endfunction
static function void report();
int unsigned total = hist_err[0] + hist_err[1];
$display("err count: %0d of %0d", hist_err[1], total);
foreach (hist_len[i])
$display("len=%0d : %0d", i, hist_len[i]);
endfunction
endclass
module top;
initial begin
txn x = new();
repeat (2000) if (!x.randomize()) $fatal(1, "txn randomize failed");
txn::report();
// A tolerance band, never an exact count: 10% of 2000 is about 200.
if (txn::hist_err[1] < 160 || txn::hist_err[1] > 240)
$fatal(1, "FAIL: %0d errors, outside the 160..240 band", txn::hist_err[1]);
$display("PASS: error rate %0d of 2000 is inside the band", txn::hist_err[1]);
$finish;
end
endmoduleerr count: 189 of 2000
len=0 : 129
len=1 : 148
len=2 : 123
len=3 : 133
len=4 : 114
len=5 : 126
len=6 : 104
len=7 : 110
len=8 : 131
len=9 : 136
len=10 : 121
len=11 : 124
len=12 : 101
len=13 : 132
len=14 : 148
len=15 : 120
PASS: error rate 189 of 2000 is inside the bandFor permanent monitoring, sample a covergroup in post_randomize instead. Skew then shows up as cold bins in the coverage report of every regression, not just when someone remembers to look.
Verilator 5.048 accepts covergroups but does not collect them, so this fragment is an illustration and the course checks skip it. Functional Coverage shows what does run.
// snippet
class txn_cov_wrap;
txn t = new();
covergroup cg;
cp_err: coverpoint t.is_err { bins err = {1}; bins ok = {0}; }
cp_len: coverpoint t.len {
bins zero = {0}; bins mid = {[1:14]}; bins max = {15};
}
x_err_len: cross cp_err, cp_len;
endgroup
function new(); cg = new(); endfunction
task gen_one(); if (!t.randomize()) $fatal(1, "randomize failed"); cg.sample(); endtask
endclassThe LRM asks for a uniform pick over the joint solution space, but tools only approximate it, so measure. Constraint structure also shapes marginal distributions in non-obvious ways.
The classic case is an implication where one branch admits far more solutions. is_small -> val < 4 on a 10-bit val leaves 4 tuples with is_small == 1 and 1024 with it clear, so a uniform pick sets is_small about once in 257 calls. Bidirectional Solving measures exactly this shape and its fixes.
A soft constraint that conflicts with any hard constraint is dropped entirely and silently. In layered benches the base author believes the default holds while the test author believes the override is narrow.
Width and sign bugs masquerade as skew. Check declared ranges before blaming the solver: unrepresentable values clip without warning.
len is 4 bits and constrained inside {[8:20]}. A signed byte is constrained < 10. A soft qdepth == 8 meets an inline qdepth > 8. What do the three histograms show?len never above 15, because 16 to 20 cannot exist in 4 bits. Mostly negative bytes, because -128 to 9 is mostly below zero. And qdepth never at 8: the soft default conflicts with the inline rule and is dropped entirely.
The bench measures all three, plus the worked error-rate example from the next section, so the counts sit on one screen.
class bus_txn;
rand bit err;
rand bit [7:0] addr;
constraint err_rate_c { err dist { 1 := 1, 0 := 9 }; } // intended: 10% errors
constraint err_addr_c { err -> addr == 8'hFF; }
endclass
class bus_txn_ordered extends bus_txn;
constraint order_c { solve err before addr; }
endclass
class clipped;
rand bit [3:0] len; // 4 bits: 0..15
constraint c { len inside {[8:20]}; } // 16..20 cannot exist
rand byte signed_off; // signed: -128..127
constraint c2 { signed_off < 10; }
endclass
class cfg;
rand bit [7:0] qdepth;
constraint typical_c { soft qdepth == 8; }
endclass
err rate, dist 1:9 plus implication: 88 of 1000; with solve err before addr: 102
len inside [8:20] on 4 bits: largest len seen 15
signed_off < 10 on a signed byte: 848 of 1000 negative
soft qdepth == 8: 200 of 200 at 8 alone; 0 of 200 once qdepth > 8 is added
PASS: the clipped range, the negative byte and the eaten soft default all showed up in the countsThe negative share came out at 85%, where a uniform pick over the 138 legal values gives 93%. Nothing in the class asked for negative values: the type did. Read the declaration before blaming the solver.
An error-injection rate can measure below its dist target when an implication gives one side far fewer completions: err allows one address, a clear err allows 256. How a solver combines the two is tool-dependent, so measure.
On Verilator 5.048 the bench above measured 88 errors in 1000 calls with the dist alone and 102 with solve err before addr: close to the 10% target both times. Another tool may fall much further short; the histogram is the proof, not the theory.
// snippet
class bus_txn;
rand bit err;
rand bit [7:0] addr;
constraint err_rate_c { err dist { 1 := 1, 0 := 9 }; }
constraint err_addr_c { err -> addr == 8'hFF; }
endclass
// err=1 admits 1 addr value, err=0 admits 256. On some solvers this
// imbalance pulls the measured err rate below 10%. Measure it.
class bus_txn_fixed;
rand bit err;
rand bit [7:0] addr;
constraint err_rate_c { err dist { 1 := 1, 0 := 9 }; }
constraint err_addr_c { err -> addr == 8'hFF; }
constraint order_c { solve err before addr; }
// err is picked first honoring its dist, then addr solves inside
// the chosen branch. The histogram confirms the rate.
endclassThe soft default qdepth of 8 conflicted with a hard override and was discarded entirely. A conflicting soft constraint is dropped wholesale, never relaxed gradually.
Values 8 to 15 only. The top of the range is unrepresentable in 4 bits, so the range clips silently and the gap looks like solver bias.
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.
The error rate came out 88 in 1000 with dist alone and 102 with solve before; the signed byte was negative 848 times in 1000, where a uniform pick gives about 928.