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.

diagram
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.

systemverilog
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.

systemverilog
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:

  1. Base class carries protocol legality ONLY, split into small named blocks (valid_c, align_c) — blocks are the control API.

  2. Defaults that tests may override are soft (or knob-gated), never hard.

  3. Configuration-dependent ranges read non-rand knobs or a cfg/policy handle — set per test in build/config.

  4. Test personalities are thin subclasses overriding named blocks, installed via factory so sequences never change.

  5. Sequences add call-local intent with randomize() with, and use rand_mode/constraint_mode for temporary phases — always restoring state.

diagram
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.