Part 3 · Constraint Randomization · Intermediate

Constraint Policy Classes

Composable policy objects carrying constraints that reference the transaction, mixing policies per test, and comparison with inheritance.

The composition problem inheritance cannot solve

Subclassing handles one axis of variation cleanly. But test intent is usually multi-axis: address region x burst character x error mix. With inheritance you get class explosion — low_addr_long_burst_no_err_txn and seven siblings — because SystemVerilog has single inheritance and constraints compose only down the chain. The policy pattern solves this with a different mechanism: a policy is a separate object that carries constraints on another object's fields . The language permits this because constraint expressions may reference any visible field through a handle. Hold a handle to the transaction inside the policy, constrain through it, and randomize policy and transaction together.

systemverilog
class bus_txn;
  rand bit [31:0] addr;
  rand bit [3:0]  len;
  rand bit        write;
  constraint valid_c { len inside {[1:15]}; addr[1:0] == 0; }
endclass

// Base policy: holds the target handle, no constraints itself
virtual class txn_policy;
  bus_txn t;                       // handle to the object under constraint
  function void bind_txn(bus_txn t_h); t = t_h; endfunction
endclass

// Each policy = one axis of intent, constraining THROUGH the handle
class low_addr_policy extends txn_policy;
  constraint c { t.addr inside {[32'h0:32'hFFFF]}; }
endclass

class long_burst_policy extends txn_policy;
  constraint c { t.len inside {[12:15]}; }
endclass

class write_heavy_policy extends txn_policy;
  constraint c { t.write dist { 1 := 9, 0 := 1 }; }
endclass

The key semantic fact: constraints in a class may reference fields of other objects via handles, and when you randomize an object, constraints in that object referencing another object's rand fields treat... careful — the direction matters. Randomizing the policy solves the policy's rand fields; t's fields are only solved if t is also rand or randomized in the same call. The composition container below gets this right by making the policies' constraints participate in one randomize call.


Composing policies in one solve

systemverilog
class policed_txn;
  rand bus_txn    txn;             // rand handle: solved together
  rand txn_policy pol[$];          // rand queue of policy objects

  function new();
    txn = new();
  endfunction

  function void add(txn_policy p);
    p.bind_txn(txn);
    pol.push_back(p);
  endfunction
endclass
// Because txn and every policy are rand sub-objects of policed_txn,
// ONE call - assert(pt.randomize()) - gathers valid_c (from txn)
// AND every bound policy constraint into a single solver problem.

module demo;
  initial begin
    policed_txn   pt = new();
    low_addr_policy   p1 = new();
    long_burst_policy p2 = new();
    pt.add(p1);
    pt.add(p2);
    repeat (10) begin
      assert(pt.randomize());
      $display("addr=%h len=%0d", pt.txn.addr, pt.txn.len);
      // addr in [0:FFFF] AND len in [12:15] AND valid_c - composed.
    end
  end
endmodule

This is the constraint-on-external-object pattern in its standard form: the container makes both the transaction and the policy objects rand members, so a single randomize() conjoins all their constraint blocks. Policies are now mixed per test at runtime — push two policies for one test, three different ones for the next — with no new classes and no edits to bus_txn. Policies can also be disabled individually (p1.c.constraint_mode(0)) or popped from the queue between phases.


Policy vs inheritance vs inline: choosing

diagram
COMPOSITION MECHANISM COMPARISON

                    subclass        inline with     policy object
  ------------------------------------------------------------------
  composable        one chain only  per call site   N policies mixed
  (multi-axis)      (class          (verbose to     freely at runtime
                     explosion)      repeat)
  reusable across   yes (named      no (copy-       yes (named class,
  tests             class)          paste)          instantiated)
  runtime add/      no (type fixed  per-call only   yes (push/pop,
  remove            at new())                       constraint_mode)
  can override      yes (same-name  no (AND only)   no (AND only)
  parent block      block)
  factory-friendly  yes (type       n/a             yes (policies are
  (UVM)             override)                       objects too)
  typical use       recurring       one-off         multi-axis test
                    single-axis     tweaks          matrices, VIP
                    intent                          stimulus libraries

  Rule of thumb:
    1 axis, recurring        -> subclass
    1 call site, one-off     -> inline with
    multiple orthogonal axes -> policies
    need to REPLACE a block  -> subclass with same-name override
                                (the one thing policies cannot do)

The last row of the rule-of-thumb matters: policies only ever AND constraints onto the problem. If a test must relax something — replace the typicality block, widen a range — that requires named-block override (inheritance) or soft constraints in the base. Mature VIP therefore uses both: soft typicality in the base class so policies' hard constraints win automatically, and a policy library for the multi-axis intent matrix.

A production wrinkle: policy queues and null safety

systemverilog
// Defensive add() for long-lived benches:
function void policed_txn::add(txn_policy p);
  if (p == null) $fatal(1, "null policy added");
  p.bind_txn(txn);
  pol.push_back(p);
endfunction
// Remember the silent-failure rule: a null element in a rand
// object queue is silently skipped by randomize() - an unbound
// or null policy simply vanishes from the solve with no error.
// Guard at insertion, not at randomize time.

Interview angle

The probe is usually “your tests need address-region x burst-length x error-mix combinations — how do you avoid 27 subclasses?” Answer with the pattern's two pillars: constraints may reference another object's fields through a handle, and randomizing a container whose rand members include both the transaction and the policy objects merges all constraint blocks into one solve. Then place it honestly against inheritance: policies compose N orthogonal axes at runtime but can only AND — replacing or widening still needs named-block override or soft bases. Citing real-world precedent (VIP stimulus libraries and the policy/knob layering in mature UVM environments use exactly this shape) signals you have seen it outside a textbook.

Key takeaways

  • A policy is an object carrying constraints on another object's fields via a stored handle.

  • Make txn and policies rand members of one container — a single randomize() conjoins everything.

  • Policies compose multi-axis intent at runtime without class explosion or base-class edits.

  • Policies can only AND — replacing/widening a block still requires inheritance override or soft bases.

  • Choose: subclass (one recurring axis), inline (one-off), policies (orthogonal axis matrix).

Common pitfalls

  • Randomizing the policy alone and expecting the transaction to be solved — direction matters; use the container.

  • Forgetting bind_txn before randomize — null handle means the policy is silently skipped, not an error.

  • Trying to widen a base constraint with a policy — AND semantics make it narrower or UNSAT.

  • Policy classes accumulating state across tests when reused — pop/reset queues at phase boundaries.

  • 27-subclass explosion because policies were never introduced — the smell is class names with three adjectives.