Verification

SystemVerilog Casting

By CourseTron Team ·  5 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    In short: SystemVerilog gives you three casts: a static cast (type'(expr)) that converts values with no run-time check, the dynamic $cast that checks at run time whether a class handle really points to the target type, and bit-stream casting that reinterprets any fixed-size data as a flat string of bits.

    Why SystemVerilog needs three kinds of casting

    Verilog converted types silently: assign a 32-bit value to a 4-bit variable and the upper bits vanish. SystemVerilog is stricter. It has enums, structs, strings and class hierarchies, and the compiler will refuse many assignments unless you state your intent. Casting is how you state it. If you want the short overview first, see our companion page on casting in SystemVerilog; this article goes deeper on the mechanism, the failure modes and the interview traps.

    The three flavours differ in when the legality is decided. A static cast is resolved at compile time and never fails at run time. A dynamic cast defers the decision to simulation, because only then does the simulator know which object a handle holds. A bit-stream cast ignores the declared structure and works purely on bit counts.

    Three casts, three moments of truth Static cast type'(expr) int'(2.7) gives 3 Checked: compile time No run-time safety net Values, enums, widths Dynamic $cast $cast(dst, src) Returns 1 or 0 Checked: simulation time Task form errors on failure Class handles, int to enum Bit-stream cast dst_t'(src) or {>>{x}} Bits in, same bits out Checked: bit count Ignores field boundaries Structs, arrays, queues Rule of thumb: compile-time cast for values, run-time cast for objects, streaming for packing CourseTron · coursetron.com
    Static, dynamic and bit-stream casts differ in what is checked and when.

    Static casting: value conversion at compile time

    The syntax is type'(expression). The apostrophe is the giveaway, and it is often confused with the apostrophe in an assignment pattern. Typical uses:

    int   i  = int'(2.7);        // real to int, rounds to nearest: 3
    logic [3:0] n = 4'(32'hABCD); // size cast, truncates to 4'hD
    int   s  = signed'(8'hF0);   // reinterprets as -16
    state_e st = state_e'(2);    // int to enum, NO legality check
    

    Notice what is missing: any complaint. state_e'(7) is accepted even if the enum only has four legal values, and the variable then holds an out-of-range number that silently fails every case branch. That is the main reason experienced engineers prefer $cast when the source value is not a literal.

    Key fact: real-to-integer conversion rounds to the nearest value, it does not truncate. int'(2.5) is 3 and int'(-2.5) is -3. Interviewers love this because C engineers assume truncation.

    A size cast is also the clean way to avoid width-mismatch lint warnings from Synopsys VCS or Cadence Xcelium. Writing 8'(a + b) tells the tool you meant to keep only eight bits, and the intent then survives code review.

    Dynamic casting with $cast and class handles

    Class inheritance creates a subtle gap between the type of the handle and the type of the object. A handle of type Packet may point to a Packet, or to any class extended from it, such as ErrPacket. The compiler only knows the handle type. Going up the hierarchy is always safe, so it needs no cast. Going down is only safe if the object is really of the derived type, so you must ask at run time.

    Handle type versus object type Packet p; ErrPacket e; upcast: p = e; (implicit) downcast: $cast(e, p); Heap object decides the answer If p points to an ErrPacket: $cast returns 1, e is assigned If p points to a plain Packet: $cast returns 0, e is unchanged (task form: run-time error) CourseTron · coursetron.com
    An upcast is always legal; a downcast depends on the object that the base handle actually holds.
    class Packet;      rand bit [7:0] addr; endclass
    class ErrPacket extends Packet; bit corrupt = 1; endclass
    
    Packet    p;
    ErrPacket e, e2;
    
    initial begin
      e = new();
      p = e;                       // upcast: implicit, always legal
      // e2 = p;                   // compile error: no implicit downcast
      if (!$cast(e2, p))           // function form: returns status
        $fatal(1, "p is not an ErrPacket");
      $display("corrupt=%0b", e2.corrupt);
    
      p = new();                   // plain Packet now
      $cast(e2, p);                // task form: run-time error here
    end
    

    Use the function form inside if when failure is a legitimate possibility, for example a scoreboard that behaves differently for error packets. Use the task form when failure means a testbench bug, so the simulator reports it for you. Either way, a failed cast leaves the destination unchanged, which makes a stale handle a classic debugging headache.

    Key fact: a null source handle is a special case. Casting null to any class handle succeeds and assigns null, so check the handle for null separately before you dereference it.

    In UVM this pattern appears constantly: uvm_sequence_item arrives at your driver as the base type, and you downcast to your own transaction. The same idea also helps when randomizing derived items; our page on constrained randomization in SystemVerilog shows how constraints are inherited through the same hierarchy.

    $cast is also the safe way to turn an integer into an enum. $cast(st, 7) returns 0 and refuses the assignment if 7 is not a member, where the static form would just store it.

    Bit-stream casting and streaming operators

    Bit-stream casting treats any "bit-stream type" as a plain sequence of bits: packed or unpacked structs, arrays, queues of integral elements. You can cast between two such types when the total bit count matches. The classic use is mapping a structured packet onto raw bytes for a DUT interface, a file or a socket.

    typedef struct packed { bit [7:0] hdr; bit [15:0] len; bit [7:0] crc; } pkt_t;
    typedef bit [31:0] word_t;
    
    pkt_t  pkt = '{8'hA5, 16'h0010, 8'h3C};
    word_t w   = word_t'(pkt);          // same 32 bits, new type
    
    byte   q[$];
    q = {>>{pkt}};                     // pack into 4 bytes, MSB first
    {>>{pkt}} = q;                     // unpack back into the struct
    q = {<< 8 {pkt}};                  // reverse BYTE order (endianness)
    

    The streaming operator >> reads left to right, so the first field lands first. << reads from the right, reversing the order of slices whose size follows the operator: {<< 8 {x}} swaps bytes, {<< {x}} reverses individual bits. Getting little-endian output from a big-endian struct takes exactly that one-line change, which is why this topic features in many DPI and protocol-driver interviews. When the packed data crosses into C, the SystemVerilog DPI interface carries those same bit vectors.

    Streaming order: the cursor reads one byte {>>{ }} reads left to right 1 0 1 1 0 0 1 0 {<<{ }} reads right to left 1 0 1 1 0 0 1 0 Same byte 8'hB2, opposite stream order: 10110010 versus 01001101 CourseTron · coursetron.com
    The blue cursor shows {>>} emitting the MSB first; the amber cursor shows {<<} emitting the LSB first.

    Key fact: class handles cannot be streamed, because a handle is a reference, not data. To serialise a transaction, stream its fields explicitly inside a pack() method, or use uvm_object::pack_bytes.

    Choosing the right cast

    SituationUseFailure behaviour
    Change width or signedness8'(x), signed'(x)Truncates or extends silently
    Int to enum, value unknown$cast(e, v)Returns 0, e unchanged
    Base handle to derived handle$cast(d, b)Returns 0 or run-time error
    Struct to byte queue{>>{s}}Compile error if sizes differ
    Packed struct to vectorvec_t'(s)Compile error if sizes differ

    Coverage collection often depends on casts too, since sampling an enum built from raw bits can hide illegal values; see functional coverage in SystemVerilog for how illegal bins catch them. Teams doing this at scale, such as the verification groups at Avecas design services, usually wrap every downcast in a helper that logs the offending type name.

    Common mistakes and how to avoid them

    • Assuming a downcast works because the code compiles. Compilation only checks the declared types. Always test the status of $cast or accept the run-time error.
    • Using the task form in production checkers. A fatal run-time error from a legal case, such as an optional packet subtype, kills a regression of thousands of seeds. Use the function form and handle the zero.
    • Forgetting that $cast needs assignment-compatible targets. The destination must be a variable, not an expression, and both sides must be in the same inheritance tree for class handles.
    • Mixing up stream direction. If bytes look reversed on the wire, check >> against << 8 before blaming the DUT.
    • Streaming unequal widths. When the destination is larger, extra bits are left-justified and the rest is zero; when smaller, an error is raised. Match sizes deliberately.

    Practise these on a free simulator such as EDA Playground before you rely on them. Our trainers at ChipXpert run the same drills in live batches, and the full syllabus sits in our CourseTron courses.

    Ready to go beyond snippets? Join the CourseTron SystemVerilog and UVM verification course, build a class-based testbench from scratch and get interview-style feedback. Explore the courses.

    FAQ

    What is the difference between static and dynamic casting in SystemVerilog?

    A static cast, written type'(expr), is evaluated at compile time and performs no validity check. A dynamic cast, $cast, checks at run time whether the source value or object is legal for the destination type and reports the result.

    Why does assigning a base handle to a derived handle give a compile error?

    Because the compiler only knows the handle's declared type, not the object it holds. A derived handle might expose members the object does not have, so you must use $cast to verify the object at run time.

    Does $cast return 0 or throw an error when it fails?

    It depends on the form. As a function (if ($cast(d, b))) it returns 0 and leaves the destination unchanged. As a task ($cast(d, b);) it raises a run-time error.

    What does {<< 8 {x}} do?

    It streams x in 8-bit slices starting from the right, reversing the byte order while keeping the bits inside each byte unchanged. It is the standard way to convert between big-endian and little-endian layouts.

    Advertisement