Verification

SystemVerilog constraints

By CourseTron Team ·  6 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    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]:/3 would 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.

    kind dist {0:=5, 1:=90, 2:=5} 5% 90% 5% 0 = RUNT 1 = NORMAL 2 = JUMBO CourseTron · coursetron.com
    Relative weights become sampling probability — corner cases (RUNT/JUMBO) stay rare but guaranteed.

    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.

    P(flag) for flag -> val == 0, val is 4 bits 16/17 1/17 50% 50% flag=0 flag=1 flag=0 flag=1 no ordering hint solve flag before val; CourseTron · coursetron.com
    16 legal pairs have flag=0 and one has flag=1, so unguided odds are 1/17; solve flag before val makes the pick 50/50.

    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

    MechanismWins overTypical use
    Hard constraint / with{}Everything belowLegal protocol rules, per-test overrides
    soft constraintNothing (yields first)Reusable defaults in base classes
    distUniform samplingWeighting corner cases
    solve...beforeDefault probability onlyRebalancing linked variables
    Solver samples one legal point per randomize() legal set: length inside {[46:1500]} 46 1500 CourseTron · coursetron.com
    Each call lands on a different legal value — the dot is the solver's current pick moving through the constrained space.

    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.

    Advertisement