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.
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 .
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
}
endclassThe 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.
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.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 herePractical 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.