I'm working on a program that tests a digital circuit. The digital circuit can process accesses from multiple different caches, CACHE0
, CACHE1
, etc. The digital circuit can handle accesses to different memories MEM0
, MEM1
, etc. and a special kind of memory SPECIAL_MEM
. The special memory can be accessed in multiple modes.
Here's a block diagram of my setup:
The design under test (DUT) is the digital circuit I am testing.
The monitoring block looks at the input and output signals of the DUT and figures out what kind of access is going on (e.g. from CACHE0
to MEM1
). It creates one object for the current operation and sends it to the testbench.
The testbench performs different tasks on the objects it receives:
- logging of operations that are seen to allow for easy debug
- checks that certain requirements are met w.r.t. the operations (e.g. that the data written from the cache is the same as the one written to the memory)
- covers which operations were seen during the test run and stores this information in a special kind of database (e.g. we've seen that we've done accesses from
CACHE1
to all memories except toSPECIAL_MEM
)
I need to be able to model all types of accesses that can happen. If I think in term of properties that an access has, these are the cache_kind
and the memory_kind
. When the memory_kind
is SPECIAL_MEM
, though, there is also the special_mem_mode
property. In my real life project there are actually many more properties I need to handle, but I want to keep the example brief and (hopefully) understandable.
Another thing to keep in mind is that in my professional field there is an over-reliance on data objects, as opposed to using objects with polymorphic behavior. You observe the inputs of the digital circuit, waiting for changes that signal a new access, create an object that describes that access and send it to different parts of your program to do different things based on it.
I'm working in an object oriented programming language called SystemVerilog, which borrows a bit from C++ and Java. I'm not sure what the best way is to model my accesses.
I could create a base interface for an access that contains the properties that are common to all accesses:
interface class access;
pure virtual function cache_kind_e get_cache_kind();
pure virtual function mem_kind_e get_mem_kind();
endclass
I could then create a specialized interface for the access to the special memory:
interface class special_mem_access extends access;
pure virtual function special_mem_mode_e get_mode();
endinterface
The special_mem_access
interface contains the get_mem_kind()
function, though, which should always return SPECIAL_MEM
for a special_mem_access
object. This feels off, because there is no way to enforce this.
There is a programming language in my field called e, where it's possible to do something called when subtyping. I used to program in this language, but had to switch to SystemVerilog. In e we would model the accesses like this:
struct access {
cache_kind: cache_kind_e;
mem_kind: mem_kind_e;
when SPECIAL_MEM'mem_kind {
mode: special_mem_mode_e;
};
};
In this case, there is only one access
class, but the compiler knows that only objects whose mem_kind
is SPECIAL_MEM
also have the mode
property. In order to access it, the compiler has to be certain that this is the case, by forcing the user to do a cast, so it's impossible to access the mode
field for an object whose mem_kind
isn't SPECIAL_MEM
.
This mechanism of when inheritance, which relies on properties, is what probably kicked off the over-reliance on data objects. After migrating to SystemVerilog, the habits stayed the same.
Coming back to the traditional OOP world, would it be better to do away with the get_mem_kind()
function entirely and encode this in the type itself? Concretely, we would have one interface for each mem_kind
:
interface class access;
pure virtual function cache_kind_e get_cache_kind();
endinterface
interface class mem0_access extends access;
endinterface
interface class mem1_access extends access;
endinterface
// ...
interface class special_mem_access extends access;
pure virtual function special_mem_mode_e get_mode();
endinterface
This way we do away with the awkwardness of having a function in special_mem_access
that trivially returns SPECIAL_MEM
. At the same time, getting the mem_kind
by querying the type of the object is most probably a code smell. We also open up the possibility for a class to implement access
directly, instead of one of its sub-types, leading to a new inconsistency.
I can also imagine moving the get_mem_kind()
function to an interface parallel to special_mem_access
:
interface class non_special_mem_access extends access;
pure virtual function mem_kind_e get_mem_kind();
endclass
Finally, one could simply put all methods inside a single interface and document that the get_mode()
function is only meant to be used for objects whose mem_kind
is SPECIAL_MEM
:
interface class access;
pure virtual function cache_kind_e get_cache_kind();
pure virtual function mem_kind_e get_mem_kind();
pure virtual function special_mem_mode_e get_mode();
endclass
If the value of get_mode()
is not defined, the object could issue a fatal error if the function is called.
I've tried to find resource online that deal with this topic, but I haven't had much luck. I'm curious how to best model this heterogeneity in properties an object has in an OOP language. Moreover, there may be many different dimensions that cause such heterogeneity: for example, there could be another MORE_SPECIAL_MEM
that has other ways it can be accessed, there could be different access modes for SPECIAL_CACHE
, etc.