Abstract Classes and constraints?

We’ve setup a handy testbench environment based on abstract classes (specifically interface classes). The environment works well - we define our pure abstract class “API”, if you will, with an interface class, then have many concrete implementations. Our testbenches manipulate these classes through the abstract class “API”.

All’s well, and the testbenches work great. However, we’ve not used the powerful randomization and constraint solver available in SystemVerilog too much. Experimenting, I add a few “rand” keywords to my class variables, and a top-level .randomize(), and bam - I get all of my concrete classes generating (unconstrained) random data. It’s easy, and works great! Except…

Adding constraints seems to be quite problematic. Since I’m working at the top-level with JUST the abstract class, how can I constrain my objects in a contextual way? The only method that I can see is using an in-line with{} clause. However the in-line clause by its nature references concrete class variables. Since I’m using abstract classes at the top-level, there seems to be no way for me to constrain an abstract class?

I’ve an example typed up that shows what I’m referring to. But it’s a little long ~100 lines. I thought I’d post this generic question first and see if I’ve described the problem clearly enough to illicit some responses…

Thanks,

Mark

In reply to Mark Curry:
One quick terminology note: interface class are abstract, but not all abstract classes are interface classes. You can declare a virtual class with pure virtual methods as well as random variables and constraints as an abstract class. But I think your point is you’ve provided an API to set of private random variables, and the the constraint mechanism is not able to use the same API.

In general, the
randomize() with {} clause
is not an OOP-friendly construct. What you can do is create extensions to your concrete class that contain added constraints and construct the extensions in place of the concrete class object. You didn’t mention if you were using the UVM, but the UVM factory facilitates this class substitution.

In reply to dave_59:

I’m not currently using UVM, but the environment we created does support it, and it’ll eventually be used in a full UVM setup. I may take a look at the UVM factory to see how it’s done there.

I’m not sure how I’d go about extending the concrete classes just to create constraints. Currently, I have only about 4-5 concrete class definitions implementing the interface class. Some of the concrete classes are parameterized. So many class specialization “variants”. But 1000s of constructed objects. I don’t think I can think of any way of (sanely) creating/managing (many) new extended classes - just to add constraints. It all depends on the context. I need to mull this over a bit.

I’m glad you at least confirmed my suspicions, randomize() with {} clause is not OOP friendly. I just need to see if there’s any (even kludge) way of doing what I want.

Regards,

Mark

In reply to Mark Curry:

Extending a class to add constraints is simple, you just extend it and add the constraints.

class concrete_class#(type T) implements abstract_class#(T);
 rand T address;
 // methods imps
endclass

class even_constraint#(type T) extends concrete_class#(T);
constraint even {address[0] == 0;} // assumes bit 0 is LSB of type T
endclass

Once you have the factory, it’s easy to substitute the construction of even_constraint for concrete_class. (See http://go.mentor.com/yin-and-yang or http://go.mentor.com/SV-OOP session 3#)
You can also extend your API to provide lists of values to use in your concrete class

class concrete_class;

rand T address;
T address_list[];
// ... API to interact with above members
constraint {address_list.size >0 -> address inside {address_list};}

In reply to dave_59:

Thanks for the response. Extending the class to add the constraint wont work for a variety of reasons. 1. We often can’t (or don’t really know) the scope of where the class was defined in order to “extend” it. 2. More generally, as mentioned, I wish to contextualize constraint at an object level, not during any definition. I.e. on this (already constructed) obj - randomize with these constraints.

But I think you’re second response should work. I’ll work on extending my API in order to add constraint (modifier|methods). Your example is a great start. Things I’m thinking of, to me, would seem to tie the constraint solver in knots. I’m thinking of explicit function calls within the constraint block instead of just your simple address_list.size() call. The standard seems to imply that function calls are allowed here. I’ll just have to try some examples to see what works.

Thanks again.

Mark

In reply to Mark Curry:
Ok, after spending (too much) time playing with randomization constraints for the last week or so, I’m back with followups. I just can’t get things to work. I’ve reduced things down to a small(ish) example. The example does NOT contain any abstract classes, but should show where I’m having difficulties.

I first declare a parameterized class


  class foo_c #( type T = bit [ 15 : 0 ] );
    typedef foo_c #( T ) foo_ic_q [ $ ];
    rand T private_obj;
    rand foo_ic_q children;

As shown, these objects contain a private_obj, and a queue of a number of children. We use these objects throughout our testbenches for modeling our golden code and driving the DUTs.
Next, I’ve added some stuff to help us add constraints to these objects:


    typedef bit [ 31 : 0 ] limits_t[ $ ]; // First index is which constraint (we may apply more than one),
    typedef int sum_indices_t[ $ ][ $ ];  // First index is which constraint.
                                          // Second index is an index into which child member the constraint relates to
    sum_indices_t sum_indices;
    limits_t      limits;

Now we add a do_sum() functions - which sums child member private_objects (with a qualifier)

    function bit [ 31 : 0 ] do_sum( input foo_ic_q children_arg = {}, int index = 0 ); 
      bit [ 31 : 0 ] rets;
      rets = 0;
      if( ( sum_indices.size() > index ) && ( children_arg.size() != 0 ) )
      begin
        for( int i = 0; i < sum_indices[ index ].size(); i++ )
	begin  
	  int this_child_index;
	  this_child_index = sum_indices[ index ][ i ]; 
	  if( children_arg.size() > this_child_index )
	    rets = rets + children_arg[ this_child_index ].private_obj;
	end
      end
      return( rets );
    endfunction

Note, I coded this method to actual explicitly pass the child objects as function arguments, instead of just operating on the (local) class member. My thoughts here was this would enable the constraint solver to “see” the other rand members.
Now a constraint:

    constraint children_less_than 
    {  
      foreach( sum_indices[ which_constraint ] )
      (
        ( limits.size() > which_constraint ) -> ( do_sum( this.children, which_constraint ) < limits[ which_constraint ] )
      ); 
    }

Now finally, a test that puts it all together:

  typedef foo_c #( bit [ 15 : 0 ] ) foo_spec;

  foo_spec parent, child0, child1;

  initial
  begin
    parent = new();
    child0 = new();
    child1 = new();
    parent.children.push_back( child0 );
    parent.children.push_back( child1 );
    if( parent.randomize() )
      $display( "Randomize() returned child0 = 0x%0x, child1 = 0x%0x, sum = 0x%0x.", child0.private_obj, child1.private_obj, child0.private_obj + child1.private_obj );
    else
      $display( "Randomize() Failed" );

This (unconstrained) example works great. The solver is randomizing parent, and according to spec, following the rand objects of children, and randomizing them too. All’s great! Until I try and add constraints (my original problem):

    parent.sum_indices.push_back( { 0, 1 } ); // child0+child1 
    parent.limits.push_back( 32'd100 );      // ( child0+child1 ) < 100
    if( parent.randomize() )
      $display( "Randomize() returned child0 = 0x%0x, child1 = 0x%0x, sum = 0x%0x.", child0.private_obj, child1.private_obj, child0.private_obj + child1.private_obj );
    else
      $display( "Randomize() Failed" );

This fails randomize(). I can get it to occasionally pass randomize() by playing with the limits. I’ve got a feeling I understand what’s happening (the children are being independently randomized() before the parent). But at this point, I’m unsure of the best way to proceed. I can’t think of any clean ways of constraining my variables.

In reply to Mark Curry:
Thanks for the detailed example and explanation. I don’t have enough time to spend on this right now, but hopefully we can get some more ideas here.

In reply to dave_59:

Playing a bit more, I think my problem is more fundamental. By involving a function call in the constraint, the constraints no longer “interact bidirectionally”. I think this is where things are falling down. The do_sum() operation which selectively sums my random variables breaks the bidirectional nature of the constraints (As 18.5.12 in the SystemVerilog spec notes.)

I’m still stuck but think I understand what is happening…

Regards,

Mark

In reply to Mark Curry:
You are correct, the function breaks the solution space and they no longer interact simultaneously. Fortunately, someone sent me a replacement constraint that removes the function call:

constraint children_less_than { 
       foreach( sum_indices[ which_constraint ] ) {
              if ( limits.size() > which_constraint ) {
                     sum_indices[ which_constraint ].sum() with ( children[ item ].private_obj ) < limits[ which_constraint ];
              }
       }
}

In reply to dave_59:

Ahhh. I looked at the .sum() built-ins, and couldn’t figure out how to code up a qualified sum(). On my first read, I thought your example wouldn’t do it either, but on reading it a few times - yes this works! This is a creative solution.

I had pretty much thrown in the towel. Things are looking better again, I can proceed again.

Thank you very much.

–Mark