Part 3 · Constraint Randomization · Intermediate

Functions & Operators in Constraints

Function calls in constraints and their ordering implications, allowed operators, modulo/division traps, and enum/struct constraints.

Calling functions inside constraints

Constraint expressions may call functions, which is how you fold non-linear or lookup-style logic (CRC widths, popcount, alignment math) into a constraint. But a function is a black box to the solver — it cannot invert it or reason about its internals. The LRM therefore imposes an implicit ordering : random variables used as function arguments are solved first , in a separate earlier stage, then the function is evaluated, and its result is treated as a fixed state value when the remaining variables are solved. It behaves as if you had written solve args before everything-else — with the same distribution consequences.

systemverilog
class frame;
  rand bit [3:0] n_bursts;
  rand bit [9:0] total_len;

  function int min_len(bit [3:0] n);
    return n * 8;             // each burst needs >= 8 bytes
  endfunction

  constraint len_c {
    n_bursts inside {[1:8]};
    total_len >= min_len(n_bursts);   // function call in a constraint
    total_len <= 512;
  }
endclass
// Solver stages:
//   1. Solve n_bursts ALONE against its own constraints → uniform 1..8
//   2. Evaluate min_len(n_bursts) → a constant, e.g. 24
//   3. Solve total_len with total_len >= 24 && total_len <= 512
// Consequence: total_len's value can NEVER influence n_bursts —
// the bidirectionality you normally rely on is broken at the call.

Function requirements: it must be automatic (or at least side-effect free), must not consume time, and its arguments cannot include output/ref directions. The killer consequence to articulate: constraints involving function results are one-directional, so contradictions that the solver could normally dodge by adjusting the argument instead become randomize() failures or skewed distributions.


Allowed operators and the integer-math traps

Almost the full SystemVerilog expression language is legal in constraints: arithmetic, relational, logical, bitwise, shifts, concatenation, reduction, ternary. The solver handles them as relations, but two integer-arithmetic families cause most real-world failures: division/modulo and width wrap-around .

systemverilog
class align_txn;
  rand bit [7:0]  len;
  rand bit [31:0] addr;

  constraint a_c {
    len % 4 == 0;                 // fine: alignment via modulo
    addr % len == 0;              // DANGER: len could solve to 0 → div by 0
    len > 0;                      //   always pair modulo-by-rand with > 0
  }

  // Width trap: bit [7:0] arithmetic wraps mod 256
  rand bit [7:0] a, b;
  constraint sum_c {
    a + b == 300;                 // 8-bit context! 300 wraps to 44 —
                                  // solver finds a+b==44 pairs instead.
    // Fix: force a wider context:
    // (a + 32'd0) + b == 300;  or compare into an int-typed expression
  }
endclass

The wrap-around trap is sneaky because randomize() succeeds — you just get pairs summing to 44 (mod 256) rather than 300, and a scoreboard catches it weeks later. Expression width rules inside constraints are the ordinary SystemVerilog self-determined/context rules; constraints add no widening magic.


Constraining enums and packed structs

Enums randomize over their declared members only — an unconstrained rand enum never produces an encoding outside the enum's value list. You can use enum literals directly in inside and dist sets. Packed structs are just shaped bit vectors, so you can constrain individual members naturally.

systemverilog
typedef enum bit [2:0] {IDLE=0, RD=1, WR=2, RMW=4} op_e;

typedef struct packed {
  bit [3:0] prio;
  bit [1:0] ch;
  bit       urgent;
} hdr_t;

class cmd;
  rand op_e  op;
  rand hdr_t hdr;

  constraint op_c {
    op dist { RD := 4, WR := 4, RMW := 1 };   // IDLE never generated
    op != IDLE;                                // (redundant w/ dist, but explicit)
  }
  constraint hdr_c {
    hdr.urgent -> hdr.prio inside {[12:15]};   // member-level constraints
    hdr.ch != 2'b11;
  }
endclass
// Note: rand op_e op picks among {IDLE,RD,WR,RMW} — declared members only,
// even though bit [2:0] has 8 encodings. Encoding 3,5,6,7 are unreachable.
diagram
FUNCTION-IN-CONSTRAINT SOLVE PIPELINE

   ┌──────────────────────────────────────────────────────┐
   │ Stage 1: variables that feed function ARGUMENTS       │
   │   n_bursts ∈ {1..8}  (solved with only its own        │
   │   constraints — partner constraints can't push back)  │
   └───────────────┬──────────────────────────────────────┘
                   ▼  evaluate  min_len(n_bursts)  constant K
   ┌──────────────────────────────────────────────────────┐
   │ Stage 2: remaining variables                          │
   │   total_len ∈ [K : 512]                               │
   └──────────────────────────────────────────────────────┘
   one-way data flow ──► bidirectional solving is CUT here

Practical guidance

  • Prefer pure expressions over functions when the math is expressible — you keep bidirectional solving and better distributions.

  • When you must call a function, constrain its argument variables tightly in their own right; they will be solved without help from downstream constraints.

  • Guard every / and % whose right operand is rand with an explicit != 0 (or > 0) constraint.

  • Watch expression widths: sums/products of narrow fields evaluate in narrow contexts and wrap silently.

  • Use enum literals, not raw encodings, in constraint sets — survives enum re-encoding.


Interview angle

What interviewers ask

  • “Can you call a function in a constraint? What does the solver do?” — yes; argument variables are solved first, the result becomes state, bidirectionality is cut at the call. This ordering consequence is the real question.

  • “Why might addr % len == 0 fail intermittently?” — len solving to 0 → division by zero / constraint failure; pair with len > 0.

  • “What does a + b == 300 do for two byte-wide fields?” — wraps mod 256; the solver satisfies a+b==44. Tests width-rule awareness.

  • “What values can an unconstrained rand enum take?” — declared members only, never stray encodings.

  • “Why avoid functions in constraints when possible?” — broken bidirectionality, hidden solve ordering, skewed distributions, and potential randomize() failures the solver cannot route around.

Key takeaways

  • Function arguments are solved before everything else; results become fixed state — bidirectionality is cut.

  • Functions in constraints must be side-effect free, time-free, and without output/ref args.

  • Guard rand divisors and modulo operands with != 0 — the solver will happily pick 0 otherwise.

  • Constraint arithmetic follows normal width rules — narrow contexts wrap silently while randomize() succeeds.

  • rand enums draw from declared members only; constrain with literals, not encodings.

Common pitfalls

  • Expecting total_len constraints to influence n_bursts through min_len(n_bursts) — the call is one-way.

  • x % y == 0 with rand y and no y > 0 guard — division-by-zero or sporadic solve failures.

  • Byte-wide a + b == 300 silently wrapping to a + b == 44 — passes randomize, fails the scoreboard.

  • Calling a function with side effects (counters, $display) — non-deterministic evaluation count across solvers.

  • Constraining an enum by its raw bit encoding — breaks the moment someone re-encodes the enum.