Part 2 · OOP for Verification · Intermediate

Upcasting, Downcasting & $cast

Implicit upcasts, why direct downcast assignment is illegal, $cast as function vs task, and failed-cast handling in monitors and scoreboards.

Upcasting is free; downcasting needs proof

Assigning a derived handle to a base handle — an upcast — is implicit and always safe: every axi_txn IS a bus_txn, so any code expecting the base contract is satisfied. The reverse — a downcast from base handle to derived handle — is a runtime claim: 'the object this base handle references is actually an axi_txn'. The compiler cannot verify that claim; the base handle might reference a plain bus_txn or some sibling type. SystemVerilog therefore makes direct downcast assignment a compile error and routes all downcasts through $cast, which checks the object's real type at runtime.

diagram
CAST DIRECTIONS

            bus_txn (base)
               ▲          │
     UPCAST    │          │   DOWNCAST
   implicit OK │          │   needs $cast
   (derived IS │          ▼   (is it REALLY
    a base)    │       axi_txn    that type?)
               │      (derived)

  bus_txn b;  axi_txn a = new();

  b = a;          // upcast — legal, implicit
  a = b;          // COMPILE ERROR — direct downcast assignment
  $cast(a, b);    // runtime-checked downcast
                  //   object is axi_txn  a points at it
                  //   object is NOT      fails (see below)

$cast as function vs task — two failure behaviors

$cast(dest, src) has two personalities depending on how you use it. Called as a function (using its return value), it returns 1 on success and 0 on failure, leaving dest unchanged on failure — no error, you handle it. Called as a task (return value discarded), a failed cast is a runtime error from the simulator. The function form is for expected-mixed streams where some types simply do not match; the task form is for casts that must succeed, where a failure means a real bug and you want the sim to flag it.

systemverilog
bus_txn b = get_from_monitor();
axi_txn a;

// FUNCTION form — you decide what failure means
if ($cast(a, b)) begin
  $display("axi id=%0d", a.id);
end else begin
  // not an axi_txn — fine for a mixed stream, skip it
end

// TASK form — failure is a simulator runtime error
$cast(a, b);   // use when "not axi_txn here" is impossible by design

// Also works on enums (same checked-conversion idea):
typedef enum {IDLE, BUSY, DONE} state_e;
state_e s;
if (!$cast(s, int_val_from_bus))
  $error("illegal state encoding %0d", int_val_from_bus);

The failed-cast handling pattern in monitors and scoreboards

Analysis paths in layered testbenches carry base-type handles, because that is what generic ports and queues are declared with. Consumers that need derived fields — a coverage collector sampling AXI qos, a scoreboard comparing derived-only fields — must downcast on arrival. The robust pattern: function-form $cast into a local derived handle, with an explicit decision for the failure branch. Decide deliberately whether a non-matching type is normal (mixed stream — skip silently), suspicious (count and warn), or impossible (error out). Defaulting to silent skips hides wiring bugs where the wrong monitor feeds your component.

systemverilog
class axi_scoreboard;
  bus_txn exp_q[$];          // generic plumbing: base handles
  int     wrong_type_count;

  function void write_actual(bus_txn t);
    axi_txn act;

    if (!$cast(act, t)) begin
      // failure branch is a DESIGN DECISION, not an afterthought
      wrong_type_count++;
      $warning("scoreboard: non-AXI txn on AXI stream (%0d so far)",
               wrong_type_count);
      return;
    end

    compare_axi(act);        // safe: act is a real axi_txn
  endfunction

  function void report();
    if (wrong_type_count > 0)
      $error("%0d non-AXI txns reached AXI scoreboard — check wiring",
             wrong_type_count);
  endfunction
endclass

Interview angle

  • 'Why is a = b (downcast) a compile error but b = a fine?' — upcast is provably safe; downcast depends on the runtime object, so it must be checked.

  • 'Difference between $cast as task and function?' — task: failure is a runtime error; function: returns 0, dest unchanged, you handle it.

  • 'Does $cast copy the object?' — no; on success dest references the SAME object, now viewed through the derived type.

Key takeaways

  • Upcasts are implicit and safe; downcasts must go through $cast because only runtime knows the object's type.

  • $cast as function returns pass/fail with dest untouched on failure; as task, failure is a runtime error.

  • $cast transfers a reference — no object copy; success means the same object viewed through a derived handle.

  • Make the failed-cast branch an explicit decision: skip, count-and-warn, or error — silent skips hide wiring bugs.

Common pitfalls

  • Direct downcast assignment a = b — compile error; newcomers expect C-style implicit conversion.

  • Task-form $cast on a legitimately mixed stream — sim error on the first non-matching transaction.

  • Function-form $cast with the return value ignored — failure silently leaves dest at its old value (often null).

  • Casting between sibling classes — always fails at runtime; a common interview trap (see Interview Traps).