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.
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.
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.
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
endclassInterview 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).