Checked casts and type safety

Reach a derived-class member from a base handle with $cast, always use its result, and know when a virtual method is the better tool.

Lesson
4.5
Time
20 min
Runs on
Verilator
Check
5 questions

A coverage collector receives axi_txn handles and records the error beat of each error transaction. It calls void'($cast(e, t)); and then samples e.err_beat. When an ordinary read arrives, the cast fails and leaves e pointing at the previous error transaction, so its error beat is counted again. Coverage reports 40 error samples when only 12 error transactions ran.

A stale handle after a failed cast
0123objecterr_txnaxi_txnerr_txncast resulte.id49sampled err_beat25cast failed; old e used

A failed $cast leaves its target unchanged, so ignoring the result reuses the last success.

Upcasts are free, downcasts must be checked

Assigning a derived handle to a base handle is an upcast, and it is always legal: every err_txn is an axi_txn. The compiler accepts t = e; without any check.

The opposite direction, a downcast, assigns a base handle to a derived one. It is safe only if the object really is of the derived class, and the compiler cannot know that, so e = t; does not compile.

$cast(e, t) performs the downcast with a check while the simulation runs. It succeeds when the object t refers to is an err_txn or a class derived from it.

A function and a task

$cast can be called in two ways, and they fail differently.

Object t refers toFunction form: ok = $cast(e, t)Task form: $cast(e, t);
an err_txnreturns 1; e refers to the objecte refers to the object
a plain axi_txnreturns 0; e is unchangedrun-time error
nothing (t is null)returns 1; e becomes nulle becomes null

Casting a null handle succeeds and copies the null.

Use the task form only where a failure really is a bug in the testbench, because it stops the run on the first unexpected object. Use the function form wherever other classes may legitimately arrive, and act on its result every time.

Task form

$cast(e, t);                 // task: no result
cov_err_beat = e.err_beat;

The first plain read that arrives stops the run with a cast error.

Function form, result used

if ($cast(e, t))
  cov_err_beat = e.err_beat;
// plain reads are skipped

Only real error transactions are sampled.

A failed cast changes nothing

When the function form fails, the target keeps whatever it held before. That is the stale handle in the opening story, and it is worse than an error, because nothing stops.

If later code must not see an old value, set the target to null before the cast. Then a mistake shows up as a null-handle error at the first use instead of as quietly wrong data.

Declaring the target inside the smallest block that needs it helps as well. A handle that does not outlive one loop iteration cannot carry a stale object into the next one.

Casting to a class in the middle

A cast succeeds for any object of the target class or of a class derived from it. If burst_err_txn extends err_txn, then $cast(e, t) into an err_txn handle succeeds for a burst error transaction too.

That is usually what you want: a coverage point for error beats should count every kind of error transaction, including kinds added later. Cast to the most general class that has the member you need.

A cast never converts an object

A successful cast creates no object and changes no object. It only gives you a second handle, of a more specific type, to an object that already was of that type.

A plain axi_txn can never become an err_txn. If you need an error transaction built from a read, construct a new err_txn and copy the fields across.

Where casts appear

Casts appear wherever a testbench passes objects around by their base type. A mailbox of axi_txn handles, a generic queue, or a configuration table that stores every setting as one base class all hand back base handles.

The receiving code knows which derived class it expects, and $cast turns that expectation into a check. When the check fails, report the class you expected and what arrived, as Module 1 recommends for every message.

Prefer a virtual method where you can

A chain of casts that asks "is it an error transaction, a burst, a write?" grows with every new class, and every copy of the chain must be updated. That is the job virtual methods do better.

If the caller only needs an answer, such as the expected response or a description, add a virtual method to the base class and let each object answer. Keep $cast for code that truly needs data that only one derived class has, such as an error-beat coverage point.

$cast also converts integers to enum types with a check, as Module 1 showed, and the same rule applies there: use the returned status.

Try it

The loop used the function form and acted on its result, so only ID 2 was treated as an error transaction. The ignored cast failed and left e on ID 2, which is exactly the stale handle from the opening story.

Replace void'($cast(e, stream[2])); with the task form $cast(e, stream[2]); and predict what happens to the last two lines.

RUNVerilator · simulator service
Edit the code, then run it
Expected: id 1: not an error transaction id 2: error transaction, err_beat=3 id 3: not an error transaction after an ignored failure, e still refers to id 2 the cast created no object: same object=1

Check yourself

Predict the outputPredict the exact output.
class t;
endclass

class e_txn extends t;
  int err_beat;
endclass

module tb;
  t b;
  e_txn x, y;
  int ok;
  initial begin
    x = new();
    x.err_beat = 2;
    b = x;
    ok = $cast(y, b);
    b = new();
    ok = ok + $cast(y, b);
    $display("%0d %0d", ok, y.err_beat);
  end
endmodule
Conceptt is an axi_txn handle and e is an err_txn handle. What does e = t; do?
ConceptA function that needs only the expected response code currently does a $cast for each kind of transaction. What is usually the better design?
ConceptThe task form $cast(e, t); runs while t refers to a plain axi_txn. What happens?
Spot the bugEach option is the body of function automatic int err_beat_of(axi_txn t), which must return the error beat of an err_txn and -1 for any other transaction. Which option fails?

Remember

  • Upcasts (derived to base) are always legal; downcasts need $cast.
  • $cast as a function returns 0 on failure and leaves the target unchanged; as a task it reports a run-time error.
  • Always use the function result; a target from an earlier success is stale.
  • A cast creates and converts nothing: it only gives a more specific handle to an existing object.
  • Prefer a virtual method when the caller needs behaviour; keep casts for derived-only data.

Practice