In short: SystemVerilog constraints let you shape randomized stimulus declaratively — inside and dist pick legal ranges and weights, solve...before and implication order dependent decisions, and soft constraints bend only when a hard rule or an inline randomize() with would otherwise fail — so one random class can generate millions of distinct, legal transactions.
Directed tests check the cases you already thought of. Constrained-random verification (CRV) checks the cases you didn't — and SystemVerilog's constraint language is the engine that makes that practical. Instead of writing a stimulus value by hand, you describe the space of legal values and let a constraint solver (the one inside Synopsys VCS, Cadence Xcelium, or Siemens Questa) pick a point from it on every randomize() call. Done well, a single transaction class covers combinations no human would enumerate. Done carelessly, you get illegal packets, biased distributions, or solver failures that silently stop randomization. This article walks the constructs that matter in practice.
The anatomy of a constrained class
Randomization starts with rand (or randc for cyclic, permutation-ordered values) class members and one or more constraint blocks. Calling obj.randomize() returns 1 on success and assigns every rand member a value that satisfies all active constraints simultaneously. The solver treats the blocks as a single system of relations, not a sequential script.
class eth_frame;
rand bit [10:0] length; // payload bytes
rand bit [1:0] kind; // 0=RUNT 1=NORMAL 2=JUMBO
rand bit [47:0] dst;
constraint c_len { length inside {[46:1500]}; }
constraint c_kind { kind dist {0:=5, 1:=90, 2:=5}; }
constraint c_jumbo { kind==2 -> length inside {[1501:9000]}; }
endclass
Key fact: Constraints are solved concurrently. The solver does not read top to bottom — c_jumbo can influence length even though it is written after c_len. Think in relations, not steps.
inside — membership and ranges
inside is set membership. x inside {[46:1500]} is far clearer than x>=46 && x<=1500, and it accepts a mix of singletons, ranges, and other variables: x inside {0, 4, [8:15], max}. Negate it with !(x inside {...}) to carve out forbidden values. Because the bounds can be variables, you can tie one random field to another's legal window without hard-coding numbers.
dist — shaping the distribution
Pure uniform randomization wastes cycles. dist assigns relative weights so corner cases appear often enough to matter. Two operators:
:=gives the weight to each value in a range.len dist {0:=1, [1:9]:=1}makes value 0 and each of 1..9 equally likely (10 equal buckets).:/splits the weight across the whole range.len dist {0:=1, [1:9]:/9}spreads weight 9 over the nine values, so each gets 1 — identical odds to the:=form here, but[1:9]:/3would give the range a combined weight of 3.
Weights are relative, not percentages: {0:=5, 1:=90, 2:=5} means 5/100, 90/100, 5/100. Bump the NORMAL weight to 900 and the ratio becomes ~0.5% / 99% / 0.5% without touching the others.
Implication and if-else — conditional law
The implication operator -> says "if the left side holds, the right side must too." kind==2 -> length inside {[1501:9000]} only bites when kind is JUMBO; otherwise it is vacuously true. Implication is one-directional, but the solver still uses it bidirectionally in the sense that choosing a short length can exclude kind==2. For mutually exclusive branches use if (cond) {...} else {...} inside a constraint block, which reads more naturally for multi-way logic.
solve...before — steering the solver, not the result
When two random variables are linked, the solver's internal ordering changes the probability distribution even though every outcome stays legal. Classic example:
rand bit flag;
rand bit [3:0] val;
constraint c { flag -> val == 0; }
Without guidance, the solver considers all legal (flag,val) pairs equally. There are 16 pairs with flag=0 and only 1 with flag=1, so flag=1 appears ~1/17 of the time. Add solve flag before val; and the tool picks flag first with 50/50 odds, then solves val — now flag=1 shows up half the time. solve...before is a hint about ordering; it never makes an illegal result legal, and it cannot resolve a true contradiction.
Key fact: randc and solve...before cannot be combined on the same variable, and ordering hints must not form a cycle. A solve a before b; solve b before a; loop is a compile/elaboration error in VCS, Xcelium, and Questa.
soft constraints and randomize() with
A soft constraint expresses a default preference that the solver will discard if — and only if — honoring it would make the problem unsolvable. This is what makes reusable base classes composable: the base sets sensible defaults softly, and a test overrides them with a hard inline constraint.
class txn;
rand int len;
constraint c_def { soft len inside {[1:64]}; }
endclass
...
txn t = new();
// inline hard constraint wins; soft default is dropped silently
assert(t.randomize() with { len == 512; });
randomize() with { ... } adds temporary constraints for one call only — ideal for a single directed-ish scenario without subclassing. Hard inline constraints override soft ones; two conflicting hard constraints instead cause randomization to fail and return 0 (always check the return value, or wrap in an assert). You can also disable a named block at runtime with obj.c_def.constraint_mode(0); and toggle a variable's randomness with rand_mode().
Precedence at a glance
| Mechanism | Wins over | Typical use |
|---|---|---|
Hard constraint / with{} | Everything below | Legal protocol rules, per-test overrides |
soft constraint | Nothing (yields first) | Reusable defaults in base classes |
dist | Uniform sampling | Weighting corner cases |
solve...before | Default probability only | Rebalancing linked variables |
Mastering these primitives is the difference between a testbench that explores state space and one that merely repeats itself. For the methodology layer on top — functional coverage closing the loop so you know what the random stimulus actually hit — see our companion piece on constrained-random verification, and the deeper treatment of functional coverage in SystemVerilog. These skills map directly to UVM sequence work taught across our verification courses. Teams building production IP often pair this with silicon design support from Avecas VLSI design services.
Ready to write real UVM sequences with constraints that hold up in regression? Join CourseTron's Functional Verification track — hands-on labs on VCS and Questa, from first randomize() to coverage-driven closure.
Frequently Asked Questions
Does solve...before change which values are legal?
No. It only changes the probability of each legal combination by fixing the order the solver resolves variables. Every outcome it produces also satisfies the constraints without the hint — the hint just rebalances how often each appears.
When should I use a soft constraint instead of a hard one?
Use soft for defaults you want callers to be able to override — typically in a reusable base transaction or sequence item. Use hard constraints for rules that are never negotiable, like a protocol's legal field encodings. A hard inline randomize() with silently overrides a conflicting soft constraint but will fail against a conflicting hard one.
What is the difference between := and :/ in dist?
:= applies the stated weight to every value in a range individually; :/ divides the weight evenly across all values in the range. For a single value the two are identical — the distinction only matters for ranges.
Why does randomize() return 0?
It returns 0 when the constraints are mutually unsatisfiable (an over-constrained or contradictory system). Always check the return value or wrap the call in an assertion; a silently ignored failure leaves stale values in the object and produces misleading tests.