Tests need to restrict a transaction in many independent ways: keep the address inside a window, cap the length, force word alignment, avoid a reserved region. The classic answer is a subclass per restriction, which is fine until tests need them in combination.
Approach | Classes for 4 independent rules | Adding a fifth rule |
|---|---|---|
One subclass per combination | Up to 16 (2^4) | Doubles the count |
Linear inheritance chain | 4, but only one fixed order of rules | Every chain that wants it must be edited |
Inline randomize() with | 0, but the rule is retyped in every test | Copy and paste into each test |
Policy classes | 4, combined freely per test | One new class, nothing else changes |
A policy is a constraint packaged so that it can be applied to an item it was not written inside. Tests pick the policies they want and the solver honours all of them together. Inline with constraints cannot be stored and reused, and constraint_mode() only switches blocks that already exist in the class.
The form usually called a policy class was presented at DVCon 2015 (John Dickol, "SystemVerilog Constraint Layering via Reusable Randomization Policy Classes"). The item holds a rand queue of policy handles. Each policy holds a handle back to the item, and its constraints refer to the item's fields through that handle. Because the queue is rand, the solver includes every policy object and its constraints in the same solve as the item.
// not-run: Verilator 5.048 does not honour a constraint that reaches the item through a back-reference (item.addr) or that is declared in a subclass of a rand member's declared type; Icarus 13 has no constraint solver
typedef class packet;
virtual class policy_base #(type ITEM = int);
protected ITEM item;
function void set_item(ITEM item); this.item = item; endfunction
endclass
class addr_window extends policy_base #(packet);
bit [15:0] lo, hi;
function new(bit [15:0] lo, bit [15:0] hi); this.lo = lo; this.hi = hi; endfunction
constraint c_win { item.addr inside {[lo:hi]}; }
endclass
class max_len extends policy_base #(packet);
bit [7:0] lim;
function new(bit [7:0] lim); this.lim = lim; endfunction
constraint c_len { item.len <= lim; }
endclass
class packet;
rand bit [15:0] addr;
rand bit [7:0] len;
rand policy_base #(packet) pcy[$]; // rand: policies join the solve
constraint c_legal { len > 0; }
function void pre_randomize();
foreach (pcy[i]) pcy[i].set_item(this); // bind before the solver runs
endfunction
endclass
// In a test:
// addr_window w = new(16'h1000, 16'h10FF);
// max_len m = new(8);
// p.pcy = '{w, m};
// void'(p.randomize());Three details carry the pattern. The queue must be rand, or the policy constraints are not part of the solve. The back-reference is set in pre_randomize(), which the LRM calls before solving, so one policy object can be shared by many items. The queue holds base-class handles, and the solver must apply the constraints of each object's actual class.
That last point is where the free tools stop. In our runs Verilator 5.048 ignored constraints declared in a subclass when the rand member's declared type was the base class, and when a policy constrained item.addr through the back-reference, randomize() still returned 1 but left the item's fields unrandomized, violating every constraint. That silent success is why an independent check of the results matters. The code above is legal SystemVerilog and is how teams use the pattern on commercial simulators; it cannot be run here.
The same reuse is available with a parameterized class that extends its own type parameter, often called a mixin. Each policy is written once as a layer, and a typedef stacks the layers a test needs. The object's class then contains every chosen constraint, so no back-reference and no polymorphic rand member is involved. Verilator runs this form.
Runtime policy list | Compile-time layers | |
|---|---|---|
Choose policies | At run time, per object | At compile time, per typedef |
Policy values | Fields of the policy object | Fields of the layer, set at run time |
Switch one off | Remove it from the list | constraint_mode(0) on that layer's block |
Needs from the tool | Polymorphic rand members, back-references | Ordinary inheritance |
Runs on Verilator 5.048 | No | Yes |
The bench below composes three layers onto a plain packet. The driver-side handle is a packet, so nothing downstream knows which policies are active. A separate function restates every rule procedurally and checks each of 400 results, so a missing policy shows up as a violation rather than as a quiet change in the distribution.
class packet;
rand bit [15:0] addr;
rand bit [7:0] len;
constraint c_legal { len > 0; }
endclass
// Each policy is written once, as a layer that can sit on top of any base
// that has the fields it constrains. Limits are runtime fields, not parameters.
class addr_window #(type BASE = packet) extends BASE;
bit [15:0] win_lo = 16'h0000, win_hi = 16'hFFFF;
constraint c_win { addr inside {[win_lo:win_hi]}; }
endclass
class max_len #(type BASE = packet) extends BASE;
bit [7:0] len_max = 8'd255;
constraint c_maxlen { len <= len_max; }
endclass
class word_aligned #(type BASE = packet) extends BASE;
constraint c_align { addr[1:0] == 2'b00; }
endclass
// One line composes three policies. No dma_aligned_short_packet class.
typedef word_aligned #(addr_window #(max_len #(packet))) dma_packet;
The second phase switches the alignment policy off with constraint_mode(0) and requires that unaligned addresses then appear. That proves the knob works and that the check is not passing because the solver happens to favour aligned values.
Mutant run. Dropping the word_aligned layer from the typedef (and the constraint_mode call that names it) makes the bench fail with 165 alignment violations in the first phase. The independent check, not the constraint, is what catches it.
Many tests, few rule sets: compile-time layers. Each test picks a typedef, and constraint_mode() handles per-phase changes.
Rules chosen from configuration or a sequence library at run time: runtime policy lists, on a simulator that supports them.
Either way: keep each rule in exactly one class, give it a procedural twin for checking, and keep the item free of test-specific constraints.
In UVM the runtime form usually sits inside a sequence item, and the sequence pushes policies before start_item(); the class structure is the same.
They are not part of the solve. randomize() only includes member objects declared rand, so the item is randomized as if no policies were attached and the test silently loses its rules.
A return of 1 only says the solver found a solution for the constraints it saw. If a policy is missing, dropped or overridden, the solver still succeeds. Only an independent restatement of the rules notices that the result breaks a rule the test asked for.