SystemVerilog Casting: static'(), $cast and bit-stream

SystemVerilog casting is how you tell the simulator to treat a value as a different type: a real as an integer, a wide vector as a packed struct, or a base-class handle as the derived class it actually points to. SystemVerilog gives you two distinct mechanisms, and choosing the wrong one is a common source of silent bugs and compile errors.

This page walks through all four forms you will meet in real RTL and verification code: the static cast type'(expr), signedness casts, the dynamic $cast used for class downcasting, bit-stream casting between aggregate types, and exactly when $cast returns 0. Every runnable block below was executed before publishing.

Two kinds of casting

SystemVerilog splits casting into static and dynamic. A static cast is resolved at compile time and always “succeeds” at the language level, even if the result loses information. A dynamic cast is checked at run time and can fail, which is why it is written as a system function ($cast) that returns a success flag.

The rule of thumb: use a static cast when the conversion is between related scalar or aggregate types whose sizes you already understand, and use $cast when correctness depends on a value that is only known while the simulation runs, such as the real object class behind a handle or an enum built from an arbitrary integer.

Static casting with type'(expression)

The static cast prefixes a parenthesised expression with a type name and an apostrophe: int'(x), byte'(x), color_e'(x). It performs a defined, size-aware conversion. A real-to-integer cast rounds to the nearest whole number; a cast into a narrower integral type keeps the low-order bits; a cast into an enum type reinterprets the numeric value as a member.

module tb;
  typedef enum logic [1:0] {RED, GREEN, BLUE} color_e;
  initial begin
    real    r;
    int     i;
    byte    b;
    color_e c;
    r = 3.75;
    i = int'(r);          // real -> int, rounds to nearest
    b = byte'(300);       // keeps low 8 bits: 300 & 255
    c = color_e'(2);      // numeric value -> enum member
    $display("int'(3.75)  = %0d", i);
    $display("byte'(300)  = %0d", b);
    $display("color_e'(2) = %s", c.name());
    $finish;
  end
endmodule
int'(3.75)  = 4
byte'(300)  = 44
color_e'(2) = BLUE
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpwb7zktiz/static_cast.sv:15: $finish called at 0 (1s)

Note that int'(3.75) gives 4, not 3: real-to-int casting rounds rather than truncates. byte'(300) gives 44 because 300 does not fit in eight bits and the upper bits are dropped. The enum cast is unchecked here, so if you write color_e'(5) you get an out-of-range enum with no error; that is the trade-off of a static cast.

Signedness casts

Two special static casts, signed'() and unsigned'(), reinterpret the same bits under a different sign interpretation without changing the bit pattern. They are essential when a vector is stored as unsigned but must participate in a signed comparison or arithmetic.

module tb;
  initial begin
    logic [3:0] v;
    v = 4'b1100;
    $display("plain bits          = %0d", v);
    $display("signed cast value   = %0d", signed'(v));
    $display("unsigned cast value = %0d", unsigned'(signed'(v)));
    $finish;
  end
endmodule
plain bits          = 12
signed cast value   = -4
unsigned cast value = 12
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpwyoacm_p/signed_cast.sv:8: $finish called at 0 (1s)

The pattern 1100 reads as 12 when unsigned and as -4 when signed, and casting the signed value back to unsigned restores 12. The bits never moved; only the interpretation changed.

$cast for dynamic and class downcasting

Object handles follow inheritance rules. Assigning a derived handle to a base handle (an upcast) is always legal and needs no cast, because every Circle is a Shape. Going the other way (a downcast, base handle into derived handle) is only legal if the object really is of the derived class, and the compiler cannot prove that statically. That is the job of $cast.

module tb;
  class Shape;
    virtual function string kind();
      return "shape";
    endfunction
  endclass

  class Circle extends Shape;
    int radius;
    function new(int r);
      radius = r;
    endfunction
    function string kind();
      return "circle";
    endfunction
  endclass

  initial begin
    Shape  s;
    Circle c, c2;
    int    ok;
    c = new(5);
    s = c;                 // upcast: always legal, no cast needed
    ok = $cast(c2, s);     // downcast: s really holds a Circle -> ok = 1
    $display("downcast ok = %0d", ok);
    if (ok) $display("radius = %0d", c2.radius);
  end
endmodule

$cast(dest, source) checks at run time whether source‘s actual object is assignment-compatible with dest‘s type. Because s was built from a Circle, the downcast succeeds and c2.radius becomes reachable. $cast can be called two ways: as a function returning 1 or 0, or as a statement ($cast(dest, source);) that raises a fatal run-time error if the conversion is not legal. Use the function form whenever failure is a possibility you want to handle gracefully.

When $cast returns 0

The function form of $cast returns 0 instead of aborting when the conversion is not valid, and it leaves the destination unchanged. Understanding the failure cases is the whole point of using it.

module tb;
  class Animal;
  endclass
  class Dog extends Animal;
    function void bark();
      $display("woof");
    endfunction
  endclass
  class Cat extends Animal;
  endclass

  initial begin
    Animal a;
    Dog    d;
    Cat    k;
    int    ok;
    k = new();
    a = k;                 // a now points at a Cat object
    ok = $cast(d, a);      // view that Cat through a Dog handle
    if (ok) d.bark();
    else    $display("cast failed, ok = 0, d stays null");
  end
endmodule

Here a holds a Cat, and Cat is not a Dog, so the downcast cannot succeed; ok becomes 0 and d stays null. $cast returns 0 in these situations:

  • The source object’s actual class is not the destination class or a subclass of it (sibling or unrelated class).
  • The source handle is null and the destination is a non-null-requiring context, or the dynamic type simply does not match.
  • A numeric or enum conversion is out of the legal value range, when the cast is written in the checked dynamic form.
  • The two types are fundamentally incompatible for assignment even after reinterpretation.

Because the destination is untouched on failure, always test the return value before dereferencing. A pattern of if (!$cast(d, a)) return; guards against null-handle dereferences that would otherwise crash the simulation.

Bit-stream casting

A bit-stream cast converts between types that occupy the same number of bits but have different structure, for example a flat vector and a packed struct, or a packed struct and an unpacked array. The bits are laid out left to right and repacked into the target type. This is invaluable for modelling instruction decode, serialisation, or protocol framing.

module tb;
  typedef struct packed {
    logic [7:0]  opcode;
    logic [7:0]  addr;
    logic [15:0] data;
  } instr_t;
  initial begin
    logic [31:0] word;
    instr_t ins;
    word = 32'hA5_10_BEEF;
    ins  = instr_t'(word);   // bit-stream cast: vector -> packed struct
    $display("opcode = %02h", ins.opcode);
    $display("addr   = %02h", ins.addr);
    $display("data   = %04h", ins.data);
    ins.opcode = 8'h01;
    word = 32'(ins);         // reverse: struct -> 32-bit vector
    $display("repacked = %08h", word);
    $finish;
  end
endmodule
opcode = a5
addr   = 10
data   = beef
repacked = 0110beef
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp_tn5fi1h/bitstream_cast.sv:18: $finish called at 0 (1s)

The 32-bit word splits cleanly into the three struct fields because the field widths sum to exactly 32. The highest-order bits map to the first struct member, so opcode takes A5, addr takes 10, and data takes BEEF. Casting back to a 32-bit vector re-flattens the struct. If the source and target bit widths differ, a bit-stream cast is a compile error for fixed-size types, which is a useful safety net that static scalar casts do not give you.

Static cast vs $cast

AspectStatic cast type'()$cast()
ResolvedCompile timeRun time
Can failNo (may lose data silently)Yes (returns 0 or errors)
Typical useNumeric, enum, signedness, bit-streamClass downcasting, checked enum
Return valueThe converted value1 on success, 0 on failure (function form)
On mismatchTruncates or reinterpretsLeaves destination unchanged

Reach for the static cast when you know the shapes line up and you simply want a reinterpretation. Reach for $cast whenever the validity of the conversion depends on information that only exists at run time, and always honour its return value.

Quick check

What does int'(3.75) evaluate to, and why?

It evaluates to 4. A real-to-integer static cast rounds to the nearest whole number rather than truncating toward zero, so 3.75 rounds up to 4.

Why can you assign a Circle handle to a Shape handle without a cast, but not the reverse?

Upcasting is always type-safe because every Circle is a Shape, so the compiler allows it directly. Downcasting may be invalid because a Shape handle might point at some other subclass, so it must be checked at run time with $cast.

Name two situations in which the function form of $cast returns 0.

When the source object’s actual class is not the destination class or a subclass of it (for example a sibling class), and when a checked numeric or enum conversion is out of the legal value range. In both cases the destination is left unchanged.

What happens if you bit-stream cast a 32-bit vector into a packed struct whose fields sum to 24 bits?

For fixed-size types the widths must match, so the cast is a compile-time error. This size check is a safety advantage of bit-stream casting over plain scalar static casts, which silently truncate.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Compile and simulate this in the browser — no install. Edit the code and re-run instantly.

Open in Verification Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD