Skip to content

Type Casting & Conversion

Static cast, $signed, $unsigned, $cast, implicit conversions, width truncation.

Module 2 · Page 2.9

When Types Meet: The Silent Conversions That Cause Real Bugs

Type conversion in SV happens in two forms: explicit (you write a cast) and implicit (the compiler does it for you). Explicit casts are visible in code review and make intent clear. Implicit conversions are invisible and follow rules that are often counterintuitive — especially when signed and unsigned types mix, when 4-state values meet 2-state variables, or when a wider value gets assigned into a narrower container.

The most common silent bugs: assigning a 32-bit int result into a 16-bit shortint (upper 16 bits silently dropped), mixing logic signed and logic unsigned in a comparison (signed value promoted to unsigned, making -1 appear larger than 1), and storing an X-valued logic into a bit (X silently becomes 0, hiding the uninitialized state).

Four Cast Mechanisms — Each With a Different Job

SystemVerilog provides four distinct casting mechanisms, each designed for a specific conversion scenario. Using the right one communicates intent clearly to both the compiler and the reader.

  • Static cast: type'(expr) — Converts any expression to the specified type at compile time. Truncates or zero/sign-extends as needed. Used for width changes, packed↔logic, real↔integer, enum↔int. No runtime check.
  • $signed() / $unsigned() — Reinterprets the sign context of a value without changing any bits. Does not convert data — only tells the compiler "treat this value as signed/unsigned in this expression context."
  • $cast(dest, src) — Dynamic cast with runtime validation. Used for class handle downcasting and enum validation. Returns 1 on success, 0 on failure. The only cast that can fail without crashing.
  • Implicit conversion — Happens automatically on assignment when types differ. No explicit syntax. Follows LRM rules: widening is safe, narrowing drops MSBs, 4→2 state drops X/Z, signedness follows the operation context.

Complete Casting Reference

SystemVerilog — All Cast Forms
// ── 1. STATIC CAST: type'(expression) ────────────────────────────
 
// Width cast — narrows or widens
logic [31:0] wide = 32'hAABBCCDD;
logic [7:0]  narrow = 8'(wide);      // keep lower 8 bits: DD
logic [15:0] mid    = 16'(wide);     // keep lower 16 bits: CCDD
 
// Integer type cast
real   r = 3.9;
int    i = int'(r);             // truncates: i = 3 (NOT 4)
real   back = real'(i);         // promotes: back = 3.0
 
// Signedness cast — same bits, different interpretation
logic [7:0] raw = 8'hFF;
int          as_s = int signed'(raw);    // -1 (sign-extended)
int          as_u = int unsigned'(raw);  // 255
 
// Enum cast (bypasses validation — use $cast for safety)
typedef enum logic [1:0] { A, B, C } abc_t;
abc_t val = abc_t'(2'b01);              // = B — no validity check
 
// Packed struct ↔ logic
typedef struct packed { logic[7:0] hi; logic[7:0] lo; } word_t;
word_t   w  = word_t'(16'hABCD);  // {hi:AB, lo:CD}
logic [15:0] lv = 16'(w);             // ABCD
 
// ── 2. $signed() / $unsigned() ────────────────────────────────────
// Reinterpret sign context WITHOUT changing bits
 
logic [7:0] u = 8'hFF;   // unsigned: 255
$display("%0d", u);               // 255 (unsigned context)
$display("%0d", $signed(u));   // -1  (same bits, signed context)
 
logic signed [7:0] s = -1;   // signed: -1 = 8'hFF
$display("%0d", s);               // -1
$display("%0d", $unsigned(s));  // 255
 
// Arithmetic right shift on unsigned type — needs $signed
logic [7:0] raw2 = 8'hF8;          // -8 if treated as signed
$display("%0d", raw2 >>> 2);       // 62 — unsigned: zero-fill
$display("%0d", $signed(raw2) >>> 2); // -2 — sign-fill
 
// ── 3. $cast(dest, src) — dynamic, validated ─────────────────────
 
// Enum validation
abc_t e;
int   n = 5;
if ($cast(e, n))
$display("cast OK: %s", e.name());
else
$display("cast FAIL: %0d not a valid abc_t", n);   // prints this
 
// Class hierarchy downcast
// class Base; endclass
// class Derived extends Base; endclass
// Base b = new Derived();   // upcast is implicit and safe
// Derived d;
// if (!$cast(d, b)) $error("not a Derived instance");
 
// ── 4. $bits() — query bit width ─────────────────────────────────
$display("$bits(logic [7:0]) = %0d", $bits(logic[7:0]));  // 8
$display("$bits(int)         = %0d", $bits(int));          // 32
$display("$bits(word_t)      = %0d", $bits(word_t));       // 16
Cast formSyntaxRuntime check?Use for
Static width castN'(expr)NoExplicit truncation or widening
Static type casttype'(expr)NoType reinterpretation: enum, struct, int↔logic
Sign reinterpret$signed(x) / $unsigned(x)NoChange sign context for comparison/shift only
Dynamic cast$cast(dest, src)Yes — returns 0/1Enum validation, class hierarchy downcast
Implicit conversionAssignment a = bNoAutomatic by compiler — be aware of rules

Visual — Every Conversion Scenario and Its Result

Width Conversion Rules

ConversionSourceDestinationWhat happensResult
Widen unsigned8'hFF16-bitZero-extend MSBs16'h00FF
Widen signed8'hFF (-1)16-bit signedSign-extend MSBs16'hFFFF (-1)
Narrow (any)16'hABCD8-bitUpper 8 bits dropped8'hCD (AB lost)
Same width, sign change8'hFF (unsigned=255)8-bit signedSame bits, signed view-1
4-state → 2-state8'hXXbit [7:0]X/Z → 08'h00 (X hidden)
real → int3.9intTruncate toward zero3 (NOT 4)
int → real42realExact promotion42.0

$signed() vs $unsigned() — Bits Don't Change, Meaning Does

ExpressionBit patternUnsigned valueSigned value
8'hFF1111_1111255-1
$signed(8'hFF)1111_1111 (unchanged)-1 (in signed context)
$unsigned(-1)1111_1111 (unchanged)255 (in unsigned context)
8'h801000_0000128-128
8'h7F0111_1111127+127

Implicit Conversion Rules Summary

ScenarioImplicit ruleSafe?
Assign narrow to wide (same sign)Zero or sign extendSafe
Assign wide to narrowTruncate MSBs — no warningData loss
logic → bit (X/Z present)X→0, Z→0 silentlyHides bugs
int → logic (4-state)2→4 state: value preservedSafe
Mix signed/unsigned in expressionSigned operand promoted to unsignedComparison flips
real to int assignmentTruncate toward zeroDepends on intent
Assign string to logicCompile errorN/A

Code Examples — From Width Casts to Class Downcasting

Example 1 — Beginner: Static Casts and Sign Functions

Example 1 — Type Cast Basics
module tb_cast_basics;
 
initial begin
 
// ── Width narrowing (explicit) ────────────────────────────────
logic [31:0] wide = 32'hAABBCCDD;
logic [7:0]  low8 = 8'(wide);       // explicit: keep lower 8
$display("8'(AABBCCDD) = 0x%02h", low8);   // DD
 
// ── Width widening ────────────────────────────────────────────
logic [7:0]  narrow_u = 8'hFF;
logic [15:0] wide_u   = 16'(narrow_u);   // 16'h00FF (zero-extend)
$display("zero-ext 8'hFF: 0x%04h", wide_u);  // 00FF
 
logic signed [7:0]  narrow_s = -1;      // 8'hFF
logic signed [15:0] wide_s   = 16'(narrow_s); // 16'hFFFF (sign-extend)
$display("sign-ext -1:   0x%04h", wide_s);   // FFFF
 
// ── $signed: reinterpret as signed without changing bits ──────
logic [7:0] u = 8'hFF;
$display("unsigned: %0d", u);             // 255
$display("signed:   %0d", $signed(u));   // -1
 
// ── 4-state → 2-state: X silently becomes 0 ──────────────────
logic [7:0] x_val = 8'hXX;
bit   [7:0] b_val  = x_val;              // X → 0 silently!
$display("logic X → bit: 0x%02h", b_val); // 00
 
// ── real ↔ int ────────────────────────────────────────────────
real r = 3.9;
int  n = int'(r);
$display("int'(3.9)  = %0d", n);   // 3 (truncates)
n = int'($round(r));
$display("round(3.9) = %0d", n);   // 4 (rounded)
 
$finish;
end
 
endmodule

Example 2 — Intermediate: Signed/Unsigned Context in Comparisons

Example 2 — Signed/Unsigned Promotion in Comparisons
module tb_sign_context;
 
initial begin
 
// ── Mixed signed/unsigned comparison trap ─────────────────────
int          s  = -1;   // signed: -1
int unsigned u  = 1;    // unsigned: 1
 
// Comparing signed and unsigned: signed promoted to unsigned!
// -1 as unsigned = 0xFFFFFFFF = 4294967295
if (s > u)
$display("-1 > 1 = TRUE  (signed promoted to unsigned!)");
 
// FIX: cast both to same sign type
if ($signed(s) > $signed(u))
$display("-1 > 1 signed = TRUE  (still -1 > 1 when both signed)");
else
$display("-1 < 1 signed = correct");   // prints
 
// ── $signed for arithmetic right shift on unsigned type ───────
logic [7:0] raw = 8'hF8;   // 1111_1000 = -8 if signed
 
$display("raw >>> 2 (unsigned): %0d", raw >>> 2);            // 62
$display("raw >>> 2 (signed):   %0d", $signed(raw) >>> 2);  // -2
 
// ── Signed extension when widening ────────────────────────────
logic signed [7:0]  s8  = -10;     // 8'hF6
logic signed [15:0] s16;
 
s16 = s8;           // implicit: sign-extends → 16'hFFF6 = -10
$display("s8=-10 → s16=%0d (0x%04h)", s16, s16);  // -10, FFF6
 
s16 = 16'(s8);      // explicit static cast: same sign-extend result
$display("16'(s8) = %0d", s16);  // -10
 
$finish;
end
 
endmodule

Example 3 — Verification: $cast for Enum and Class Hierarchy

Example 3 — $cast for Enum Validation and Class Downcast
// ── Enum $cast ────────────────────────────────────────────────────
typedef enum logic [1:0] { IDLE, RUN, DONE } state_t;
 
function automatic bit safe_set_state(ref state_t s, input int raw);
if (!$cast(s, raw)) begin
$error("Invalid state value %0d — ignoring", raw);
return 0;
end
$display("State set to: %s", s.name());
return 1;
endfunction
 
// ── Class hierarchy $cast ─────────────────────────────────────────
class base_txn;
int id;
function new(int i); id = i; endfunction
endclass
 
class axi_txn extends base_txn;
logic [31:0] addr;
function new(int i, logic[31:0] a); super.new(i); addr=a; endfunction
endclass
 
module tb_cast_demo;
initial begin
state_t s;
 
safe_set_state(s, 1);   // OK: RUN
safe_set_state(s, 3);   // ERROR: 3 not in IDLE/RUN/DONE
 
// Upcast (implicit, always safe)
base_txn base_h = new axi_txn(1, 32'hA000);  // OK: axi_txn IS-A base_txn
 
// Downcast (needs $cast — may fail at runtime)
axi_txn axi_h;
if ($cast(axi_h, base_h)) begin
$display("Downcast OK: addr=0x%08h", axi_h.addr);  // A000_0000
end else
$error("Not an axi_txn");
 
// Downcast to wrong type — $cast returns 0, no crash
base_txn plain_h = new base_txn(99);
if (!$cast(axi_h, plain_h))
$display("Downcast FAIL: plain_h is not an axi_txn");  // prints
 
$finish;
end
endmodule

Example 4 — Corner Case: Coverage Percentage, $bits(), Packed Struct Cast

Example 4 — Practical Cast Patterns
module tb_cast_patterns;
 
typedef struct packed {
logic [7:0] hi;
logic [7:0] lo;
} word16_t;
 
initial begin
 
// ── Integer division → real (cast before dividing!) ───────────
int covered = 75, total = 100;
$display("WRONG: %0.1f%%", covered / total * 100.0);       // 0.0%
$display("RIGHT: %0.1f%%", real'(covered) / real'(total) * 100.0); // 75.0%
 
// ── Packed struct ↔ logic vector ──────────────────────────────
word16_t w;
logic [15:0] raw = 16'hABCD;
 
w = word16_t'(raw);    // raw → struct: hi=AB, lo=CD
$display("hi=%02h lo=%02h", w.hi, w.lo);
 
raw = 16'(w);          // struct → raw: ABCD
$display("raw = 0x%04h", raw);
 
// ── $bits() for dynamic width query ──────────────────────────
$display("$bits(word16_t) = %0d", $bits(word16_t));  // 16
$display("$bits(int)      = %0d", $bits(int));       // 32
$display("$bits(real)     = %0d", $bits(real));      // 64
 
// ── Longint cast to avoid sum overflow ────────────────────────
int counts [4] = '{1500000000, 1500000000, 1, 1};
int     bad_sum  = counts.sum();                          // OVERFLOW
longint good_sum = counts.sum() with (longint'(item));   // safe
$display("bad_sum  = %0d", bad_sum);   // wrong (negative)
$display("good_sum = %0d", good_sum);  // 3000000002
 
$finish;
end
 
endmodule

Simulation Behavior — When Does Conversion Happen?

Expression Context Determines Sign

In SV, the signed/unsigned context of an expression is determined by the operands, not the destination. If any operand in a binary expression is unsigned, the entire expression evaluates in unsigned context — even if both operands have the same bit width and one is declared signed. The destination type does not influence how the expression is computed, only how the result is stored.

This is why $signed(x) must be applied before the operation, not after. Writing $signed(x >>> 2) applies the shift first (in unsigned context because x is unsigned) then reinterprets the result as signed. The correct form is $signed(x) >>> 2 — shift in signed context.

ExpressionContextResult for x=8'hFF
x >>> 2 (x is logic [7:0])Unsigned8'h3F = 63 (zero fill)
$signed(x) >>> 2Signed8'hFE = -1... wait: 8'hFF = -1 >>> 2 = 8'hFF = -1
logic signed [7:0] s = x; s >>> 2Signed8'hFE = -1 (sign-fill)
x + 1 (both unsigned)Unsigned8'h00 = 0 (overflow wrap)
$signed(x) + 1Signed8'h00 = 0 (same bits, signed context)

Where Casting Appears in Real Verification Work

Verification Patterns Using Casts
// ── 1. COVERAGE PERCENTAGE: cast before divide ────────────────────
function automatic real coverage_pct(int covered, total);
return (real'(covered) / real'(total)) * 100.0;
endfunction
 
// ── 2. PACKET FIELD EXTRACTION: struct cast from raw ──────────────
ctrl_reg_t ctrl = ctrl_reg_t'(bus_readback);
$display("timeout=%0d", ctrl.timeout);
 
// ── 3. SIGNED COMPARISON IN SCOREBOARD ───────────────────────────
logic [31:0] dut_result;
int           exp_signed = -10;
// Compare treating DUT output as signed
if ($signed(dut_result) !== exp_signed)
$error("Signed mismatch: exp=%0d got=%0d",
exp_signed, $signed(dut_result));
 
// ── 4. UVM DOWNCAST (pattern used everywhere in UVM) ──────────────
// In a UVM scoreboard write() task:
// task write(uvm_sequence_item item);
//   axi_seq_item txn;
//   if (!$cast(txn, item))
//     `uvm_fatal("CAST", "Expected axi_seq_item")
//   check_txn(txn);
// endtask
 
// ── 5. LATENCY MEASUREMENT: int → longint for large values ────────
longint t_req = $time;
// ... wait ...
int latency = int'($time - t_req);  // safe if latency < 2B cycles
 
// ── 6. STROBE-MASKED BYTE COMPARE ─────────────────────────────────
function automatic bit bytes_match_with_strobe(
logic [31:0] exp, got, logic [3:0] strb
);
for (int i=0; i<4; i++) begin
if (strb[i]) begin
logic[7:0] e = 8'(exp >> (i*8));  // extract byte via cast
logic[7:0] g = 8'(got >> (i*8));
if (e !== g) return 0;
end
end
return 1;
endfunction

Classic Cast Bugs — Each One Has Bit Someone

Bug 1 — Integer Division Before Real Cast: Always 0%

Bug 1 — Cast After Division Is Too Late
int covered = 75, total = 100;
 
// BUGGY: integer division happens first → 75/100 = 0 → 0 * 100.0 = 0.0
real pct = covered / total * 100.0;
$display("Coverage: %0.1f%%", pct);   // 0.0% — wrong!
 
// FIXED: cast at least one operand BEFORE the division
pct = real'(covered) / real'(total) * 100.0;
$display("Coverage: %0.1f%%", pct);   // 75.0% — correct

Bug 2 — Silent Narrowing: Upper Bits Dropped Without Warning

Bug 2 — Implicit Narrowing Truncates MSBs
int    wide_result = 32'hAABBCCDD;
byte   narrow;
 
// BUGGY IMPLICIT: upper 24 bits silently dropped — no warning
narrow = wide_result;   // keeps only lower 8: DD
$display("implicit: 0x%02h", narrow);  // DD — looks valid, upper bits gone
 
// EXPLICIT: same result but intent is clear
narrow = byte'(wide_result);  // explicit truncation
$display("explicit: 0x%02h", narrow);  // DD
 
// CORRECT if you need to detect overflow:
if (wide_result > 127 || wide_result < -128)
$error("Value doesn't fit in byte: %0d", wide_result);

Bug 3 — $signed() Applied After the Operation

Bug 3 — Wrong Placement of $signed()
logic [7:0] data = 8'hF8;   // = -8 when interpreted as signed
 
// BUGGY: $signed() applied AFTER shift — shift was already unsigned
logic signed [7:0] wrong = $signed(data >>> 2);
$display("wrong: %0d", wrong);   // 62 — unsigned shift first, then reinterp
 
// CORRECT: $signed() before the shift — shift executes in signed context
logic signed [7:0] right = $signed(data) >>> 2;
$display("right: %0d", right);   // -2 — arithmetic right shift

Bug 4 — Using Static Cast Instead of $cast for Enum: No Safety Check

Bug 4 — Static enum Cast Bypasses Validation
typedef enum logic [1:0] { IDLE, RUN, DONE } state_t;
state_t s;
int     raw = 3;   // 3 is NOT a valid state (only 0,1,2 defined)
 
// BUGGY: static cast — no validation, illegal value silently stored
s = state_t'(raw);       // compiles; s.name() returns "" — illegal state
$display("name: '%s'", s.name());  // "" — red flag!
 
// CORRECT: $cast — validates and reports failure
if (!$cast(s, raw))
$error("Cannot set state to %0d — not a valid state_t value", raw);
// $error fires — no illegal state reaches the FSM

Interview Questions

Beginner Level

Q1: What does $signed(x) do? Does it change the bits in x?$signed(x) tells the compiler to treat the expression x as a signed value in the current operation context. It does not change the actual bits stored in x — the bit pattern remains identical. It only affects how the compiler interprets the value for operations like comparison, arithmetic right shift, and sign extension during widening. After the expression is evaluated, the original variable x retains its original type and value. Q2: What happens when a logic variable holding X is assigned to a bit variable? X is silently converted to 0. The bit type is 2-state and has no X or Z value — the closest representable value for X is 0. This conversion happens silently with no warning. This is the core reason why using bit for interface signals or reference model variables is dangerous: an uninitialized DUT output (X) looks like a valid driven zero in the testbench.

Intermediate Level

Q3: What is the difference between int'(x) and $cast(dest, x)?int'(x) is a static cast — compile-time, no validation, always succeeds. It reinterprets or converts the value silently. $cast(dest, x) is a dynamic cast — runtime, with validation. It checks whether the conversion is valid (e.g., whether an integer value corresponds to a defined enum member, or whether a base class handle actually points to a Derived object) and returns 0 if not. For enum assignments and class hierarchy downcasting, always use $cast to get runtime safety.

Experienced Engineer Level

Q4: An engineer writes logic signed [7:0] s_result = $signed(raw_byte) >>> 2 expecting arithmetic right shift, but the result is wrong. What is the mistake? The issue is operator precedence combined with expression evaluation order. The expression $signed(raw_byte) >>> 2 evaluates left-to-right as intended — $signed() applied before the shift, giving signed context. This should actually work correctly. The common mistake that produces wrong results is when the engineer writes $signed(raw_byte >>> 2) — applying $signed()after the shift. In this case, the shift executes first in unsigned context (zero-fill from MSB), then the result is reinterpreted as signed. The sign-fill never happened. The fix: always apply $signed() to the input of the operation, not the output.

Best Practices & Coding Guidelines

  • Make narrowing explicit — Whenever assigning a wider type to a narrower variable, use an explicit cast: narrow = 8'(wide_val). Implicit narrowing is silent data loss — the cast makes the truncation visible in code review.
  • Cast before integer divide — Any division that expects a fractional result: cast at least one operand to real first. real'(a) / real'(b) — never a / b when the result is assigned to a real variable.
  • $signed() before the operation — Apply sign context before arithmetic/shift operations, not after. $signed(x) >>> n is correct. $signed(x >>> n) shifts first in unsigned context — the sign-fill never happens.
  • $cast for enums and class hierarchy — Never use static type'() for enum assignment from a runtime variable — it bypasses validation. Always use $cast() and check the return value for safety.
TaskCorrect approachCommon mistake
Truncate wide to narrownarrow = 8'(wide) (explicit)narrow = wide (silent truncation)
Integer to real for divisionreal'(a) / real'(b)a / b * 1.0 (int divide first)
Arithmetic right shift on unsigned$signed(x) >>> n$signed(x >>> n) (too late)
Assign runtime int to enum$cast(e, n) + checke = enum_t'(n) (no safety)
Downcast class handle$cast(derived, base) + checkDirect cast — crashes if wrong type
Sum large int arrayarr.sum() with (longint'(item))arr.sum() — int overflow

Summary — Chapter 2 Complete

Type casting ties together everything in Chapter 2. Every type covered — integer, real, string, enum, struct, union — eventually needs to interact with another type, and the rules governing those interactions determine whether your simulation correctly models the intended behavior or silently produces wrong results. Most dangerous conversions are implicit and silent; the casts in this section make them explicit and intentional.

  • type'(expr) — static cast. No runtime check. Used for width changes, real↔int, packed struct↔logic. Makes intent explicit; no safety.
  • $signed(x) / $unsigned(x) — sign context. Same bits, different interpretation. Apply before the operation.
  • $cast(dest, src) — dynamic cast. Runtime validation. Returns 0 on failure. Use for enum assignment from variables and class hierarchy downcasting.
  • Narrowing is always silent data loss. Make it explicit with a cast to show you intended it.
  • 4-state → 2-state silently converts X/Z to 0. Never use bit for interface signals or reference models — it hides uninitialized hardware states.
  • Cast before integer division when you expect a fractional result: real'(a) / real'(b).