Four Problems with Policy-Based Constraints and How to Fix Them
Originally presented at DVCon US 2024
Table of contents
Constraints and Policy Class Review
Random objects and constraints are the foundational building blocks of constrained random verification in SystemVerilog. The simplest implementations embed fixed constraints within a class definition. Embedded constraints lack flexibility; all randomized object instances must meet these requirements exactly as they are written.
In-line constraints using the with
construct offer
marginally better flexibility. Although these external constraints allow
greater variability of random objects, their definitions are still fixed
within the calling context. Furthermore, all in-line constraints must be
specified within a single call to randomize()
.
Policy classes are a technique for applying SystemVerilog constraints in a portable, reusable, and incremental manner, originally described by John Dickol [1][2]. The operating mechanism leverages an aspect of "global constraints," the simultaneous solving of constraints across a set of random objects. Randomizing an object that contains policies also randomizes the policies. Meanwhile, the policies contain a reference back to the container. Consequently, the policy container is constrained by the policies it contains. Dickol's approach is illustrated by the following code.
These two base classes provide the core definitions for policies:
policy_base
implements the hook back to the policy
container, and policy_list
enables related policies to be
organized into groups. Both classes are parameterized by a container
object type, so a unique specialization will be required for each
policy-enabled container. Policy containers like transactions,
sequences, and configuration objects implement these classes to support
flexible random steering. Below are examples of a generic transaction,
addr_txn
, with a random address and size and some policy
classes to constrain those attributes.
The addr_permit_policy
and
addr_prohibit_policy
classes implement some policies for
constraining addr_txn
addresses. Address ranges can be
stored in the ranges
array. The
addr_permit_policy
will choose one of the ranges at random
and constrain the address to be within the range, while the
addr_prohibit_policy
will exclude addresses that fall
within any of the ranges in its list.
The final class shows how policies might be used. The
addr_constrained_txn
class extends addr_txn
and defines two policies, one that permits the address to be within one
of two ranges, and one that prohibits the address from being within a
third range. The addr_constrained_txn
class then passes the
local pcy
list to the parent policy
queue.
At this point, an instance of addr_constrained_txn
can be
created and randomized like normal, and the address will be constrained
based on the embedded policies.
Further work on policy-based constraints has been presented since the original DVCon presentation in 2015. Kevin Vasconcellos and Jeff McNeal applied the concept to test configuration and added many nice utilities to the base policy class [3]. Chuck McClish extended the concept to manage real number values for User Defined Nettypes (UDN) and Unified Power Format (UPF) pins in an analog model [4]. Additionally, McClish defined a policy builder class that was used to generically build multiple types of policies while reducing repeated code that was shared between each policy class in the original implementation.
Although there has been extensive research on policies, this paper aims to address and provide solutions for three issues that have not been adequately resolved in previous implementations. Furthermore, a fourth problem that arose during testing of the upgraded policy package implementation will also be discussed and resolved.
Problem #1: Parameterized Policies
The first problem with the above policy implementation is that because
policy_base
is parameterized to the class it constrains,
different specializations cannot be grouped and indexed. The awkward
consequences of this limitation become apparent when using policies with
a class hierarchy. If you extend a class and add a new random field then
you need a new policy type to constrain that field. The new policy type
requires its own policy list, and the new list must be traversed and
mapped back to the container during pre_randomize()
.
Imagine a complex class hierarchy with several layers of inheritance and
extension, and constrainable attributes on each layer (one common
example is a multi-layered sequence API library, such as the example
presented by Jeff Vance [5]). Using policies becomes cumbersome in
this case because each class layer requires a unique family of
constraints organized into a distinct list. This stratification of
polices places a burden on users to know which layer of the class
hierarchy defines each attribute they want to constrain, and the name of
the associated policy list for the matching policy type. For example,
extending the addr_txn
class to create a version with
parity checking results in a class hierarchy that looks like this:
Scaling this implementation results in a lot of repeated boilerplate code and is not very intuitive to use. Each additional subclass in a hierarchy only increases the chaos and complexity of implementing and using policies effectively.
The solution to this problem is to replace the parameterized policy base with a non-parameterized base and a parameterized extension. We chose an interface class as our non-parameterized base for the flexibility it offers over a virtual base class---specifically, our policies are bound only to implement the interface functions and not to extend a specific class implementation.
A non-parameterized base enables all policies targeting a particular
class hierarchy to be stored within a single common
policy_queue
. A parameterized template,
policy_imp
, implements the base interface and core
functionality required by all policies.
One consequence of eliminating the parameter from our base type is that
the policy-enabled container object, item
, and its
assignment function, set_item()
, are no longer strongly
typed. Here we make a small concession, using uvm_object
as
our default policy-enabled type. This means that all classes that
implement our policies must derive from uvm_object
, and we
need to use dynamic casting to ensure that policies and their containers
are type-compatible. In the example above, item
is set to
null
and randomization is disabled when the cast fails,
preventing runtime problems in the event that incompatible policies are
applied.
Not much changes when it comes to defining policies; the address
policies now extend policy_imp
instead of
policy_base
, and the underlying constraints are written as
implications so that they will not apply when item
is
missing.
However, the address transaction classes are simplified considerably.
Only a single policy queue is required in the class hierarchy, and the
vast majority of the boilerplate code has been eliminated, including all
of the specialized lists. The addr_txn
class now extends
uvm_object
to provide compatibility with the policy
interface.
Problem #2: Definition Location
The second problem with policies is "where do I define my policy classes?" This is not a complicated problem to solve; most users will likely wish to place their policy classes in a file or files close to the class they are constraining. However, directly embedding policy definitions within the class they constrain offers a myriad of benefits. Not only does this convention eliminate all guesswork about where to define and discover policies, but embedded policies also gain access to all members of their container class, including protected properties and methods! This privileged access enables policies to constrain attributes of a class that are not otherwise exposed, improving encapsulation.
To further optimize the organization of potentially large families of
policies, we establish a convention of defining all policy classes
within an embedded wrapper class called POLICIES
. Each
layer of a class hierarchy that implements policies will have its own
embedded POLICIES
wrapper, and individual
POLICIES
wrappers extend other wrappers in a manner
parallel to their container classes. This parallel inheritance pattern
is shown below, with addr_p_txn::POLICIES
extending
addr_txn::POLICIES
.
This example also shows how we can define static constructor functions
within POLICIES
wrappers. This practice further reduces the
cost of using policies since we can instantiate and initialize them with
a single call, as demonstrated with the call to
addr_constrained_txn::POLICIES::PARITY_ERR()
. Note that
although the PARITY_ERR
constructor is defined in
addr_p_txn::POLICIES
, it is accessible through
addr_constrained_txn::POLICIES
because of the wrapper class
inheritance. The POLICIES::
scoping layer even helps to
make code more readable and easy to understand.
What's more, the parity_err
property has now been defined
as protected
, preventing anything but our
PARITY_ERR
policy from manipulating that "knob." In fact,
a more advanced use of policies might define all members of a target
class as protected, restricting the setting of fields exclusively
through policies and reading through accessor functions, thus
encouraging maximum encapsulation/loose coupling, which reduces the cost
of maintaining and enhancing code and prevents bugs from cascading into
classes that use policy-enabled classes.
Problem #3: Boilerplate Overload
The third problem with using policies is that policies are relatively
expensive to define since you need at a minimum: a class definition, a
constructor, and a constraint. This will be relatively unavoidable for
complex policies, such as those defining a relationship between multiple
specific class attributes. For generic policies, such as equality
constraints (property equals X), range constraints (property between Y
and Z), or set membership (keyword inside
) constraints,
macros can be used to drastically reduces the expense and risk of
defining common policies. The macros are responsible for creating the
specialized policy class for the required constraint within the target
class, as well as a static constructor function that is used to create
new policy instances of the class. Additional macros are utilized for
setting up the embedded POLICIES
class within the target
class. These macros can be used hand in hand with the non-macro policy
classes needed for complex constraints, if necessary.
This example includes a `fixed_policy
macro, which
wraps two additional macros responsible for creating a policy class and
a static constructor for the class. This `fixed_policy
example policy class lets you constrain a property to a fixed value. A
more complete macro definition can be found in the appendix. The
appendix includes `start_policies
,
`start_extended_policies
, and
`end_policies
macros that are used to create the
embedded POLICIES
class within the constrained class
instead of using hard-coded class
and endclass
statements. They set up class inheritance as needed and create a local
typedef for the policy_imp
parameterized type.
The base addr_txn
class has complex policies with a
relationship between the addr
and size
fields,
so rather than creating a policy macro that will only be used once, they
can either be left as-is within the embedded policies class, or moved to
a separate file and included with `include
as was done
here to keep the transaction class simple. The child parity transaction
class is able to use the `fixed_policy
macro to
constrain the parity_err
field. The constraint block
remains the same as the previous example.
Problem #4: Unexpected Policy Reuse Behavior and Optimizing for Lightweight Policies
A fourth problem occurred during our initial deployment of policies. We
observed occasional unexpected behavior when attempting to re-randomize
objects with policies. We didn't thoroughly characterize the behavior,
but sometimes policies seemed to "remember" previous randomizations and
wouldn't reapply their constraints during subsequent
randomize
calls. Results would clearly violate even simple
policies.
Our policy architecture prioritizes scalability; policy classes are lightweight with a minimal footprint. Rather than investing effort to diagnose and work around the problem with reusing policies, we adopted a "use once and discard" approach, leaning into their disposable nature. It costs little to apply fresh policy instances before re-randomizing a target object. Following this strategy completely eliminated policy misbehavior.
To facilitate a safer form of policy reuse we introduced a
copy
method that returns a fresh policy instance
initialized to the same state as the policy that implements it. We also
doubled down on our use of static constructors to generate initialized
policies.
Passing array literals populated by policy instances from static constructors proved to be an excellent way to pack a lot of intent into little code. It also neatly worked around the reliability issues of reused policies.
More Improvements to the Policy Package
The examples presented so far are functional, but are lacking many features that would be useful in a real-world implementation. The following examples will present additional improvements to the policy package that will make it more practical and efficient to use.
Expanding the policy
interface class
The following policy
interface class adds additional
methods for managing a policy.
The name
, description
, and copy
methods are implemented by the policy (or policy macro) directly and
provide reporting information useful when printing messages about the
policy to the log for the former two, or specific behavior for making a
copy for the latter. The remaining three methods are implemented by
policy_imp
and are shared by all policies.
Better type safety checking and reporting in policy_imp
methods
Some of the benefits of above methods can be seen by examining the new
set_item
method used by policy_imp
.
The set_item
method makes use of all the reporting methods
to provide detailed log messages when set_item
succeeds or
fails. Additionally, the item_is_compatible
is used before
the \$cast
method is called and the rand_mode
state is kept consistent with the result of the cast.
Replacing policy_list
with policy_queue
Eagle-eyed readers might have noticed the lack of presence of a
policy_list
class in any of the examples above after
migrating to the improved policy interface. Rather, a single typedef is
all that is necessary to manage policies in a class.
The policy_queue
type is capable of storing any policy that
implements the policy
interface. The default queue methods
are sufficient for aggregating policies, and in practice we found that
using policy queues as containers was more efficient than
policy_list
instances. For example, for functions expecting
a policy_queue
argument we can directly pass in array
literals populated by calls to static constructor functions, allowing us
to define, initialize, aggregate, and pass policies all in a single line
of code!
Standardize policy implementations with the policy_container
interface and policy_object
mixin
The policy_container
interface class defines a set of
functions for managing policies using policy_queue
arguments. These functions provide a simple, standard way to implement
policies across a verification environemnt.
The policy_object
mixin implements the
policy_container
interface and contains a protected
policy_queue
for managing policies (a complete example is
available in the appendix).
The policy_object
mixin can be applied to any class that
might benefit from the use of policies, as seen in the following
examples.
Protecting the policy queue enforces loosely coupled code
A subtle but significant additional benefit to using a base
policy_object
class along with the
policy_container
API is the ability to mark the container's
policy queue as protected and prevent direct access to it.
The original implementation called set_item
on each policy
during the pre_randomize
stage. That was necessary because
the policy_queue
was public, so there was nothing to
prevent callers from adding policies without linking them to the target
class.
Using an interface class and making the implementation private means the
callers may only set or add policies using the available interface class
routines, and because those routines are solely responsible for applying
policies, they can also check compatibility (and filter incompatible
policies) and call set_item
immediately. This can be seen
in the example policy_object
implementation above, in the
usage of the protected function try_add_policy
.
By calling set_item
when the policy is added to the queue,
all policies will be associated with the target item automatically, so
there is no need to do it during pre_randomize
. This
eliminates an easily-overlooked requirement for classes extending
policy_object
to make sure they call
super.pre_randomize()
in their local
pre_randomize
function.
Conclusion
The improvements to the policy package presented in this paper provide a more robust and efficient implementation of policy-based constraints for SystemVerilog. The policy package is now capable of managing constraints across an entire class hierarchy, and the policy definitions are tightly paired with the class they constrain. The use of macros reduces the expense of defining common policies, while still allowing great flexibility in any custom policies necessary.
A functional package is available for download [6] which can be included directly in a project to start using policies immediately.
References
Appendix: Source Code
The full policy package implementation is found in the following repository: https://github.com/DillanCMills/policy_pkg