Problem: a transaction factory

Specify and build a function that creates the right transaction class from a name chosen at run time, then test every rule it promises.

Lesson
4.7
Time
30 min
Runs on
Verilator
Check
5 questions

A test reads +mix=rd,wr,err from the command line and must build the right class for each name. The first version copied a case statement with three constructors into each of four generators. When the error class was added, one generator was missed, so the error test quietly generated plain reads for a week and its error checks never ran.

The missed generator
0123requested kindrderrwrerrclass builtrd_txnwr_txnrd_txnexpected_resp()OKAYerror requested, read built

Without one place that maps names to classes, every copy of the mapping can drift.

The task

Write create(kind, id), a function that returns an axi_txn handle. The string kind names the class to build, and id is the transaction ID the new object must carry.

kindClass builtexpected_resp()
"rd"rd_txnOKAY
"wr"wr_txnOKAY
"err"err_txnSLVERR
anything elsenone: returns nullnot applicable

The mapping the factory owns.

Four rules complete the specification. Every successful call returns a new object, never one returned before. The object carries the requested ID. Names are matched exactly, so "RD" is unknown. An unknown name returns null, and the caller reports it.

The return type also limits what callers may assume. Through an axi_txn handle they can use only base members and virtual methods, so everything a caller needs must be declared in axi_txn.

Why a factory helps

A factory is a function whose only job is to create objects. Callers ask it for "an error transaction" and receive an axi_txn handle, without ever naming the derived class.

The mapping from names to classes then lives in one place. Adding a burst transaction means one new class and one new line in the factory, and every generator, test and sequence that calls create gets it at once. It is also the one place to count how many objects of each kind a test built.

The handle that comes back has the base type, so callers use virtual methods such as expected_resp() and never need to cast.

The factory can be a function in a package, or a static method of a factory class, such as txn_factory::create. A static method keeps the factory and any settings it needs together, without creating a factory object.

Building it

  1. Declare one handle per class

    Inside the function, declare rd_txn r; wr_txn w; err_txn e;. Each new call needs a handle of the exact class to build.

  2. Select on the name

    Use a case on kind. Each branch constructs its class with the requested ID and returns the handle, which converts to axi_txn automatically.

    "err"e = new(id); return e;
  3. Handle everything else

    The default branch returns null. Do not fall back to a read, because a silent fallback is exactly the bug in the opening story.

Each class passes the ID up through super.new(id), as lesson 4.3 showed, and overrides the virtual expected_resp() from lesson 4.4. The factory itself never sets a property; constructors do that.

Traps to avoid

A factory that keeps one object per kind and hands it out again on every call passes a quick test, because the class and the ID look right. Then two outstanding reads share one object, and changing one changes the other, as in lesson 4.1.

A factory that maps unknown names to a default class hides typos in test arguments. Returning null makes the caller notice, and a caller that ignores null fails loudly at its first use of the handle.

Cached object

static rd_txn cached;
if (cached == null) cached = new();
cached.id = id;
return cached;

Two reads get the same object; the second call changes the first ID.

New object per call

r = new(id);
return r;

Each call returns its own object with its own ID.

Reading the mix from the command line

The names usually come from the test command line. $value$plusargs("mix=%s", mix) reads the text after +mix=, and the test splits it at the commas and calls create for each name.

When create returns null, report the bad name together with the list of known names. A message such as unknown kind "erorr"; known: rd, wr, err lets whoever ran the test fix the command line in one step.

Tests that matter

Test each rule on its own. Create one object of each kind and check expected_resp() through the base handle. Create two of the same kind and check that they are different objects with different IDs.

Then try an unknown name, an upper-case name and an empty string, and check that each returns null. Write every expected result from the specification table before you run the code.

Finally, check the scoreboard side: an object from the factory should travel through the same queue and checks as one built directly with new.

Try it

Each name produced its own class, and every call to expected_resp() went through an axi_txn handle. The upper-case name was rejected, and two reads came back as two objects.

Add a burst_txn class whose expected_resp() returns OKAY and whose kind() returns "burst_txn", then add one line to the factory. Predict the new output line for the name "burst".

RUNVerilator · simulator service
Edit the code, then run it
Expected: rd: rd_txn id=0 expects OKAY wr: wr_txn id=1 expects OKAY err: err_txn id=2 expects SLVERR RD: unknown kind two reads: same object=0, ids 10 and 11

Check yourself

Predict the outputThis is a smaller version of the lesson's factory: it knows only "rd" and "err". Predict the exact output.
class axi_txn;
  int id;
  function new(int id = 0); this.id = id; endfunction
  virtual function string expected_resp(); return "OKAY"; endfunction
endclass

class err_txn extends axi_txn;
  function new(int id = 0); super.new(id); endfunction
  virtual function string expected_resp(); return "SLVERR"; endfunction
endclass

function automatic axi_txn create(string kind, int id);
  err_txn e;
  axi_txn r;
  case (kind)
    "rd":    begin r = new(id); return r; end
    "err":   begin e = new(id); return e; end
    default: return null;
  endcase
endfunction

module tb;
  axi_txn x, y, z;
  initial begin
    x = create("err", 4);
    y = create("rd", 5);
    z = create("wr", 6);
    $display("%s %s %0d", x.expected_resp(), y.expected_resp(), z == null);
  end
endmodule
ConceptWhy should the factory return null for an unknown name instead of building a read?
ConceptA factory passes every test that creates one object of each kind. Which extra test catches a factory that caches one object per kind?
Read the waveformThe factory caches one rd_txn and sets its ID on each call. Two reads are created and stored. What does the scoreboard's first stored read show for its ID in cycle 1?
Two reads from a caching factory
01callcreate("rd", 10)create("rd", 11)returned handleobj 1exp_q.size()12

Each returned handle is pushed onto the scoreboard queue.

Spot the bugEach option is the body of create(string kind, int id) for read and error transactions. Which option breaks a rule of the specification?

Remember

  • A factory is one function that maps a run-time name to a class and returns a base handle.
  • Keeping the mapping in one place means a new class needs one new line, not edits in every generator.
  • Return a new object on every successful call; a cached object makes transactions share state.
  • Return null for unknown names, and make the caller report them.
  • Test two objects of the same kind, not only one of each kind.

Practice