Part 3 · Constraint Randomization · Intermediate
Choosing a Control Strategy
Decision flow across inline with, constraint_mode, rand_mode, subclass override, and policy/knob classes — with test-layer examples.
Five tools, one decision
Every constraint-control mechanism answers a different question. Inline with answers “tighten just this call.” constraint_mode answers “remove this rule for a while on this object.” rand_mode answers “freeze this field across calls.” Subclass override answers “this test has a standing personality.” And knob/policy state answers “the legal space is configuration-dependent by design.” Interviews at the senior level rarely ask for one mechanism's syntax — they describe a scenario and watch which tool you reach for, and whether you can defend the choice.
CONTROL STRATEGY DECISION FLOW
Need to change generation behavior?
│
├─ Only TIGHTEN, for ONE call site?
│ └─► randomize() with { ... } [inline]
│
├─ Need to RELAX / remove a legality rule?
│ ├─ temporarily, on one object, mid-sequence?
│ │ └─► named block + constraint_mode(0) [toggle]
│ └─ as a standing policy for a whole test?
│ └─► subclass override (+ factory) [replace]
│
├─ PIN a field to a known value across many calls?
│ └─► set value, field.rand_mode(0) [freeze]
│
├─ Range/mode depends on CONFIGURATION?
│ └─► non-rand knobs / cfg object in constraints [state]
│
└─ Whole DISTRIBUTION PROFILE swappable per test,
shared by several classes?
└─► policy/knob class injected as state,
or constraint-bearing subclass family [policy]
Composition is normal: factory override sets the personality,
knobs parameterize it, inline 'with' adds per-call intent.The same requirement, five ways
Requirement: a bus test needs mostly-short bursts, occasionally pinned to one hot address, with one phase generating illegal lengths. Watch how the layers divide the work — no single mechanism covers all three needs cleanly.
class bus_txn;
int unsigned max_len = 16; // [state knob]
rand bit [31:0] addr;
rand bit [7:0] len;
constraint legal_c { len inside {[1:max_len]}; }
constraint align_c { addr[1:0] == 0; }
endclass
class short_bus_txn extends bus_txn; // [subclass personality]
constraint legal_c { len inside {[1:4]}; } // replaces: short bursts
endclass
task test_body(bus_txn t);
// phase 1 — hot-address hammering [rand_mode freeze]
t.addr = 32'h8000_0000; t.addr.rand_mode(0);
repeat (100) void'(t.randomize());
t.addr.rand_mode(1);
// phase 2 — one directed corner [inline with]
void'(t.randomize() with { len == 1; addr == 32'h0; });
// phase 3 — illegal lengths [constraint_mode + inline]
t.legal_c.constraint_mode(0);
repeat (10) void'(t.randomize() with { len inside {[17:32]}; });
t.legal_c.constraint_mode(1);
endtask
// The test installs short_bus_txn via the factory; test_body never knows.Policy classes: constraints as plug-in objects
The most reusable (and most senior-interview-flavored) pattern: package a distribution profile as its own small class, hand it to the transaction as a state handle, and let the transaction's constraints defer to it. Swapping the policy object swaps the profile — no inheritance of the transaction, no mode juggling, and one policy can serve many transaction types.
virtual class len_policy;
pure virtual function int lo();
pure virtual function int hi();
endclass
class short_policy extends len_policy;
virtual function int lo(); return 1; endfunction
virtual function int hi(); return 4; endfunction
endclass
class jumbo_policy extends len_policy;
virtual function int lo(); return 64; endfunction
virtual function int hi(); return 255; endfunction
endclass
class bus_txn;
len_policy pol; // state handle — the plug-in
rand bit [7:0] len;
constraint len_c {
pol != null -> len inside {[pol.lo():pol.hi()]};
pol == null -> soft len inside {[1:16]}; // default when unset
}
endclass
// test: t.pol = jumbo_policy_inst; → every randomize() now jumbo.
// (Function-call ordering applies: pol's results are state — fine here,
// since policies are non-rand by construction.)Policies compose with everything else: the factory can install a transaction subclass AND the env can hand it a policy AND a sequence can still tighten inline. Each layer owns one concern.
The reusable-transaction interview question
“How do you make one transaction class constrainable per-test?” is the standard senior screen. A strong answer is a layered design, delivered in order:
Base class carries protocol legality ONLY, split into small named blocks (valid_c, align_c) — blocks are the control API.
Defaults that tests may override are soft (or knob-gated), never hard.
Configuration-dependent ranges read non-rand knobs or a cfg/policy handle — set per test in build/config.
Test personalities are thin subclasses overriding named blocks, installed via factory so sequences never change.
Sequences add call-local intent with randomize() with, and use rand_mode/constraint_mode for temporary phases — always restoring state.
LAYERED CONTROL — WHO OWNS WHAT
base txn class legality blocks (valid_c, align_c), soft defaults
▲
cfg / policy obj knobs: max_len, addr windows, traffic profile
▲
test factory override → personality subclass
▲ + sets knobs/policy
sequence inline 'with' per call; temporary
rand_mode/constraint_mode (restored!)
lower layers define WHAT IS LEGAL,
upper layers decide WHAT IS INTERESTING.Interview angle
What interviewers ask
“How do you make a reusable transaction constrainable per-test?” — give the layered answer: small named legality blocks, soft defaults, knobs/policy state, factory-installed subclass personalities, inline with for call-local intent.
“When would you pick constraint_mode over a subclass override?” — temporary, instance-local, mid-sequence toggling vs a test-wide standing policy.
“Why not do everything with inline with?” — it cannot relax, it scatters policy through sequences, and it is invisible to the factory.
“What is the risk of rand_mode/constraint_mode heavy designs?” — sticky per-instance state: anything not restored leaks into the next sequence.
“What belongs in the base class vs the test?” — legality below, interest above; the base must never encode one test's preferences as hard constraints.
Key takeaways
Match the tool to the change: tighten-one-call (with), remove-rule (constraint_mode), freeze-field (rand_mode), standing personality (override+factory), config-dependent space (knobs/policy).
Inline with can never relax — any relaxation requirement immediately rules it out.
Mode mechanisms are sticky instance state; overrides are clean type-level policy with nothing to restore.
Policy/knob objects swap whole profiles without inheritance or mode juggling.
Layer the design: legality in the base, configuration in knobs, personality in subclasses, intent at call sites.
Common pitfalls
Hard-coding one test's preferences as hard constraints in the base class — every other test fights them.
Using inline with for a policy needed at fifty call sites — duplication and drift.
Leaking disabled blocks or frozen fields across sequence boundaries — restore discipline is part of the design.
Subclassing for a one-off directed value — a freeze or inline constraint is two lines, not a new type.
One giant constraint block — constraint_mode and override both become all-or-nothing, destroying the control API.