A random failure without a seed is folklore. The highest-value habit is printing the seed at the top of every log, unconditionally, and recording test, seed, simulator version, and commit hash per regression row.
Replay means the same binary, options, and seed reproduce bit-identical streams. Debug edits that add object construction or thread draws upstream of the failure shift streams and lose the repro.
The fields below are unconstrained, so no constraint solver is needed. The next listing runs under Verilator in this course's checks.
class txn;
rand bit [31:0] addr;
endclass
module top;
initial begin
txn a, b;
bit [31:0] first;
int unsigned seed;
if (!$value$plusargs("SEED=%d", seed)) seed = 1;
$display("SEED: %0d (replay with the same value)", seed);
// Manual seeding detaches an object from positional seeding.
// srandom works right after construction, or inside new()
// (the LRM's own srandom example seeds in the constructor).
a = new();
a.srandom(32'hDEAD_BEEF);
if (!a.randomize()) $fatal(1, "FAIL: a.randomize");
first = a.addr;
b = new(); // an extra construction upstream...
b.srandom(32'h1234);
a.srandom(32'hDEAD_BEEF); // ...cannot move a re-anchored object
if (!a.randomize()) $fatal(1, "FAIL: a.randomize");
if (a.addr !== first) $fatal(1, "FAIL: srandom did not pin a");
$display("PASS: the same srandom seed gives a the same addr");
$finish;
end
endmoduleSEED: 1 (replay with the same value)
PASS: the same srandom seed gives a the same addrAdding a display call is replay-safe because it draws nothing. Adding construction or sampling calls upstream is not, so pin the suspect object with srandom while debugging around it.
The worst bugs return 1. Three language rules produce most silent failures: a null nested handle is skipped without warning, rand_mode(0) persists on the object, and randc cycle state interacts with mid-cycle state changes.
The module below randomizes only txn2, which has no constraints, so it runs under Verilator in this course's checks without a constraint solver.
class payload;
rand bit [7:0] data[];
constraint sz_c { data.size() inside {[1:64]}; }
endclass
class frame;
rand bit [7:0] hdr;
rand payload pl;
function new();
pl = new(); // without this, randomize silently skips pl
endfunction
endclass
class txn2;
rand bit [31:0] addr;
rand bit [7:0] len;
endclass
module top;
initial begin
txn2 t;
t = new();
// rand_mode(0) is sticky: addr becomes state until re-enabled.
t.addr = 32'h4000_0000;
t.addr.rand_mode(0);
repeat (100) if (!t.randomize()) $fatal(1, "FAIL: txn2 randomize");
// ... phases later, addr is still pinned. Audit at boundaries:
if (t.addr !== 32'h4000_0000) $fatal(1, "FAIL: addr moved while rand_mode was 0");
if (t.addr.rand_mode() == 0)
$display("addr rand_mode is OFF entering the next phase");
t.addr.rand_mode(1);
if (t.addr.rand_mode() != 1) $fatal(1, "FAIL: rand_mode(1) did not re-enable addr");
$display("PASS: rand_mode(0) held addr for 100 calls until re-enabled");
$finish;
end
endmoduleaddr rand_mode is OFF entering the next phase
PASS: rand_mode(0) held addr for 100 calls until re-enabledA randc field cycles per object, and constraints filter the cycle. Treat any state change touching a randc field's constraints as invalidating the cycle, and build explicit shuffled queues where exact permutation order matters.
A fatal wrapper with an object dump turns every failure into a self-contained repro report. The dump prints the object's fields through convert2string, which shows the live state values, and the seed completes the kit.
A post_randomize validity check guards the output side. It catches disabled constraints and mode leftovers regardless of which silent mechanism produced them.
The bench below wraps the call in a plain if, so it still runs when assertions are disabled, and builds the report from convert2string. A first call solves; then the test sets a state bit that makes the same request impossible.
class dma_desc;
rand bit [12:0] nbytes;
bit small_ring; // state, set by the test
int post_calls;
constraint ring_c { small_ring -> nbytes <= 1024; }
function void post_randomize();
post_calls++;
endfunction
virtual function string convert2string();
return $sformatf("nbytes=%0d small_ring=%0d", nbytes, small_ring);
endfunction
endclass
// A plain if, so it still runs when assertions are disabled.
function automatic bit randomize_or_report(string who, dma_desc d, output string report);
if (d.randomize() with { nbytes >= 4000; }) return 1;
report = $sformatf("RANDOMIZE REPORT (%s): %s", who, d.convert2string());
return 0;
endfunction
module tb_loud;
dma_desc d = new();
string report;
bit [12:0] prev_nbytes;
initial begin
if (!randomize_or_report("seq0", d, report)) $fatal(1, "FAIL: first call should solve");
prev_nbytes = d.nbytes;
d.small_ring = 1; // the state change that breaks the next call
if (randomize_or_report("seq1", d, report)) $fatal(1, "FAIL: second call should fail");
$display("%s", report);
$display("nbytes after the failure: %0d (the value from the earlier success: %0d)", d.nbytes, prev_nbytes);
$display("post_randomize ran %0d times for 1 success and 1 failure", d.post_calls);
if (d.nbytes != prev_nbytes) $fatal(1, "FAIL: a failed call changed a rand field");
$display("PASS: the report names the caller and the state, and the rand field kept its stale value");
$finish;
end
endmodule%Warning-UNSATCONSTR: /var/folders/41/dgs80y1569b4x8pwflv_w9jr0000gn/T/rtbtb-K1WcxI/t52.sv:5: Unsatisfied constraint: 'constraint ring_c { small_ring -> nbytes <= 1024; }'
%Warning-UNSATCONSTR: /var/folders/41/dgs80y1569b4x8pwflv_w9jr0000gn/T/rtbtb-K1WcxI/t52.sv:16: Unsatisfied constraint: 'if (d.randomize() with { nbytes >= 4000; }) return 1;'
RANDOMIZE REPORT (seq1): nbytes=5294 small_ring=1
nbytes after the failure: 5294 (the value from the earlier success: 5294)
post_randomize ran 2 times for 1 success and 1 failure
PASS: the report names the caller and the state, and the rand field kept its stale valueThree things in that output matter in a real debug. The report names the caller and the live state, small_ring=1, which is the cause. The rand field shows the stale value from the earlier success, not anything the failed call chose. And post_randomize ran twice: Verilator 5.048 also runs it after a failed call, so a check inside it cannot tell a fresh result from a stale one unless the caller passes the return value along.
Everything compresses into one ordered procedure. Each step either resolves the problem or narrows the next step's search space.
Confirm the failure is visible: check the return value first, with a fatal wrapper if the call is bare.
Classify the symptom: return 0 means contradiction, unchanged fields mean silent skip, skew means distribution, and intermittent means seed.
Lock the repro: replay the exact test, seed, and build triple and confirm a bit-identical failure.
Dump the solver inputs: print the object state at the failure site, including mode getter status.
For return 0, isolate: read the tool's UNSAT report, run a deletion filter over named blocks, and pin suspects with an inline with.
For return 0, corner it: dump the solver core on the reduced case and build a minimal standalone repro.
For wrong values, check the silent trio first, then histogram the distribution over many calls.
For skew, apply solver semantics: count solutions, check ordering, look for eaten soft defaults and clipped ranges.
Fix at the right layer: state bugs in the test, intent conflicts via soft, disable, or subclass, never by deleting legality.
Harden: leave a fatal wrapper, a post_randomize check, and coverage on the affected fields behind.
A DMA descriptor fails on a few seeds, always mid-test. Replay locks it, the object dump shows a small ring level as state, and hand-solving finds a leftover rand_mode(0) pinning the wrap flag from an earlier phase.
Steps | What happened |
|---|---|
1 and 2 | The return value was checked: 0, so the contradiction branch. It failed on only some seeds, so a state dependency too. |
3 | Replaying seed 8841 failed at the same iteration every time. |
4 | The object dump showed the rand fields at their previous values, because failed calls assign nothing, and the state |
5 | Hand-solving: the inline |
9 | The leftover |
10 | Left behind: the loud wrapper, a cross-check in |
No. The exit criterion is hardening: a loud wrapper, a validity check, and coverage so the failure class cannot hide again.
Not necessarily. Failed calls assign nothing, so rand fields show previous values. Chase the live state values first.
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.