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.
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.
| kind | Class built | expected_resp() |
|---|---|---|
| "rd" | rd_txn | OKAY |
| "wr" | wr_txn | OKAY |
| "err" | err_txn | SLVERR |
| anything else | none: returns null | not 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
Declare one handle per class
Inside the function, declare
rd_txn r; wr_txn w; err_txn e;. Eachnewcall needs a handle of the exact class to build.Select on the name
Use a
caseonkind. Each branch constructs its class with the requested ID and returns the handle, which converts toaxi_txnautomatically."err" e = new(id); return e;Handle everything else
The
defaultbranch returnsnull. 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".
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 11Check yourself
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
- Factory by opcode Implement · Easy
- Validated factory arguments Implement · Hard