The dist operator does two jobs at once. It constrains the variable to the listed set, and it weights the pick so the solver no longer aims at uniform.
Each item probability is its weight over the sum of all weights. Weights are relative, so {a := 1, b := 3} and {a := 25, b := 75} describe the same split.
// snippet: the weight arithmetic, measured in the bench below
class arb_txn;
rand bit [1:0] grant;
// grant is 0, 1 or 2 only (3 is now ILLEGAL: dist constrains the set)
constraint grant_c {
grant dist { 0 := 1, 1 := 2, 2 := 5 };
}
endclass
// Total weight = 1 + 2 + 5 = 8
// P(grant==0) = 1/8, P(grant==1) = 2/8, P(grant==2) = 5/8
// P(grant==3) = 0: not in the dist set, never producedThe trap is the constraining half. Values outside the list vanish entirely, and a clash with another constraint fails randomize.
On a single value, := and :/ behave identically. On a range they differ: := gives the weight to each value, while :/ gives it to the range as a whole.
// snippet: the weight arithmetic, measured in the bench below
class dist_cases;
rand bit [7:0] xa;
rand bit [7:0] xb;
// Case A: := on a range is weight PER VALUE (range total 50, sum 100)
constraint a_c { xa dist { [1:5] := 10, 9 := 50 }; }
// Case B: :/ on a range splits the weight (range total 10, sum 60)
constraint b_c { xb dist { [1:5] :/ 10, 9 := 50 }; }
endclass
// Case A: P(x==1) = 10/100 = 10% each of 1..5; P(x==9) = 50/100 = 50%
// Case B: P(x==1) = 2/60, about 3.3% each of 1..5; P(x==9) = 50/60, about 83.3%A usable mnemonic is that := weights each member and :/ divides across the range. Recompute the total before quoting any probability when a range is present.
Other constraints intersect with dist as usual. The surviving weights renormalize within the final legal set, so every remaining probability shifts.
// snippet: the weight arithmetic, measured in the bench below
class err_txn;
rand bit [3:0] resp;
// 0=OKAY with most weight, 1..3 share the rest
constraint resp_c {
resp dist { 0 :/ 90, [1:3] :/ 10 };
}
endclass
// A test adds resp != 2, which removes value 2.
// Surviving weights: 0 -> 90, 1 -> 10/3, 3 -> 10/3, total about 96.67.
// P(resp==0) = 90/96.67, about 93.1%: renormalized, no longer exactly 90%Note that dist applies only to rand variables. It is illegal on randc, whose cycling already dictates the distribution.
// snippet: the weight arithmetic, measured in the bench below
class weight_demo;
rand bit [7:0] v;
constraint q_c { v dist { 1 := 40, [2:4] := 60, [5:6] :/ 40 }; }
endclass
// Expand: 1 -> 40; 2,3,4 -> 60 EACH (:= is per value); 5,6 -> 20 each (:/ splits).
// Total = 40 + 180 + 40 = 260.
// P(1) = 40/260, about 15.4%; P(2..4) = 60/260 each; P(5..6) = 20/260 each.
// All other values of v have probability 0: dist constrains the set.Step | Routine for exact shares |
|---|---|
Expand | Turn each range into per-value weights using the |
Sum | Add every item weight into one total. |
Divide | Quote each probability as item weight over that total. |
:= and :/ on the range [1:5] with weight 10?:= gives 10 to each of the five values, for a range total of 50. :/ gives 10 to the range as a whole, so 2 per value.
No. The surviving weights renormalize over the smaller set, so each remaining probability moves.
xb dist { [1:5] :/ 10, 9 := 50 } is solved 2,500 times. About how often is xb 9?About 83% of the time, roughly 2,080 of 2,500. With :/ the range shares its 10, so the total is 60 and 9 holds 50 of it.
Each dist from this lesson, solved 2,500 times, with the measured share printed beside the weight arithmetic.
class dist_cases;
rand bit [1:0] grant;
rand bit [7:0] xa, xb;
rand bit [3:0] resp;
constraint grant_c { grant dist { 0 := 1, 1 := 2, 2 := 5 }; } // 3 is now illegal
constraint a_c { xa dist { [1:5] := 10, 9 := 50 }; } // := weight per value
constraint b_c { xb dist { [1:5] :/ 10, 9 := 50 }; } // :/ weight split over the range
constraint r_c { resp dist { 0 :/ 90, [1:3] :/ 10 }; }
endclass
grant == 0 (1/8) measured 12.0% predicted 12.5%
grant == 2 (5/8) measured 61.9% predicted 62.5%
xa == 9, := (50/100) measured 49.3% predicted 50.0%
xb == 9, :/ (50/60) measured 84.2% predicted 83.3%
resp == 0 (90/100) measured 89.6% predicted 90.0%
resp == 0 with resp != 2 measured 90.0% standard 93.1%
PASS: the five plain shares are within 2.5 points of the weight arithmetic, and the removed value never appearedThe last line shows the one place this simulator drifts: after resp != 2 removes a value, the standard renormalizes the remaining weights to give 93.1% for resp == 0, and the measured share stays near 90%.
One way to write it:
typedef enum bit [1:0] {DATA = 2'd0, CTRL = 2'd1, ERR = 2'd2} kind_e;
class pkt_txn;
rand kind_e kind;
rand bit [3:0] len;
constraint c_len { len inside {[1:8]}; }
// Mostly data, some control, rare errors: 85 / 12 / 3 percent.
// Written with the numeric values 0, 1, 2 (DATA, CTRL, ERR); see the evidence note.
constraint c_kind { kind dist { 0 := 85, 1 := 12, 2 := 3 }; }
endclass
module tb_pkt_kind_dist;
pkt_txn p = new();
int k[3];
int n = 3000;
initial begin
repeat (n) begin
if (!p.randomize()) $fatal(1, "FAIL: randomize failed");
k[p.kind]++;
end
$display("DATA %0d CTRL %0d ERR %0d of %0d", k[DATA], k[CTRL], k[ERR], n);
if (k[ERR] < 50 || k[ERR] > 130) $fatal(1, "FAIL: ERR share %0d is far from 3%%", k[ERR]);
if (k[DATA] < 2460 || k[DATA] > 2640) $fatal(1, "FAIL: DATA share %0d is far from 85%%", k[DATA]);
$display("PASS: kinds came out near 85 / 12 / 3 percent");
$finish;
end
endmoduleDATA 2527 CTRL 384 ERR 89 of 3000
PASS: kinds came out near 85 / 12 / 3 percentRun 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.
Measured on 8,000 and again on 2,500 solves: the five plain shares were within 1.5 points of the weight arithmetic. With resp != 2 added, Verilator gave about 90% for resp == 0 where renormalizing gives 93.1%.
Verilator 5.048 ignored dist weights written with enum names (DATA := 85): every name came out about equally often. The project writes the numeric values 0, 1 and 2, which it weighted correctly.