Part 1 · Language Foundations · Intermediate

Integer Types & Casting

byte/shortint/int/longint/integer/time, signedness, static cast vs $cast vs bit-stream cast, and truncation/extension rules.

The integer family

SystemVerilog defines fixed-width 2-state integers — byte (8), shortint (16), int (32), longint (64) — all signed by default , plus the 4-state legacy types integer (32-bit signed 4-state) and time (64-bit unsigned 4-state). The signed default is the part that bites: byte b = 8'h80; holds −128, and comparing it against an unsigned quantity drags the whole expression through signed/unsigned conversion rules. Declare int unsigned or byte unsigned whenever the value is a count, address, or bit pattern rather than a true signed number.

diagram
Legend: [TYPE]

  INTEGER FAMILY [TYPE]

  type       bits  state  signed-by-default
  ────────── ────  ─────  ─────────────────
  byte         8     2      yes
  shortint    16     2      yes
  int         32     2      yes
  longint     64     2      yes
  integer     32     4      yes   (legacy Verilog)
  time        64     4      no    (unsigned)
  bit [N:0]    N     2      no    (unsigned)
  logic [N:0]  N     4      no    (unsigned)

  Mixed signed/unsigned expression?
   if ANY operand is unsigned, the comparison/arithmetic
    context becomes UNSIGNED  -1 becomes huge positive

The classic trap: if (my_int < my_queue.size()) where my_int is −1 and size() returns an int — that one is fine; but compare against an unsigned or a sized literal and −1 silently becomes 4294967295, the comparison flips, and a loop runs zero or forever. Interviewers probe this with "what does (-1 < 8'd5) evaluate to?" — the answer depends entirely on the signedness of the operands.


Three kinds of cast

Static cast int'(expr), unsigned'(expr), 16'(expr) — is a compile-time conversion between compatible types: change width, signedness, or numeric type. It always succeeds; data may truncate or round (real→int rounds, not truncates). Dynamic cast $cast(dst, src) — is a runtime-checked downcast, used for class handles (base → derived) and for stuffing an integer back into an enum. Called as a function it returns 0 on failure; called as a task it errors. Bit-stream cast — a static cast between bit-stream types (packed structs, arrays, queues) — reinterprets the raw bits, the standard trick for serializing a struct into a byte queue.

systemverilog
// 1. Static casts — always succeed, may lose data
int          i  = int'(3.7);          // real → int: ROUNDS to 4
byte         b  = byte'(16'h1234);    // truncates to 8'h34
int unsigned u  = unsigned'(-1);      // reinterpret: 32'hFFFF_FFFF

// 2. Dynamic cast — runtime-checked
typedef enum logic [1:0] {IDLE, BUSY, DONE} state_e;
state_e st;
int     raw = 2;
if (!$cast(st, raw))                   // checks 2 is a legal enum value
  $error("raw=%0d is not a valid state_e", raw);

base_txn  bt = factory_get();          // base handle
eth_txn   et;
if ($cast(et, bt))                     // succeeds only if bt holds an eth_txn
  et.crc_check();

// 3. Bit-stream cast — reinterpret packed bits
typedef struct packed { logic [7:0] cmd; logic [23:0] addr; } hdr_t;
hdr_t            h = '{cmd: 8'hA5, addr: 24'h00_1000};
logic [31:0]     w = logic'(h);        // struct → plain vector
byte             q[$] = {<< byte {h}}; // struct → byte queue (streaming)

Truncation and extension on assignment

Plain assignment between different widths needs no cast at all — SystemVerilog silently truncates from the MSB side when the target is narrower and extends when it is wider: zero-extension if the source is unsigned, sign-extension if the source is signed. Most lint tools flag implicit truncation precisely because the language will not. The rule to internalize: signedness of the source decides extension; width of the target decides truncation .

systemverilog
logic [3:0]  nib;
logic [15:0] wide;
byte signed  sb = -2;            // 8'hFE

nib  = 16'hABCD;                 // silent truncation → 4'hD
wide = nib;                      // zero-extend (nib unsigned) → 16'h000D
wide = sb;                       // SIGN-extend → 16'hFFFE  (not 16'h00FE)
wide = byte'(sb) & 8'hFF;        // mask idiom if you wanted 16'h00FE

Cast selection guide

  1. Changing width/signedness/numeric type of values — static cast, or rely on assignment rules where lint allows.

  2. Downcasting a class handle or int→enum — $cast, and always check the return value in a function-call form.

  3. Serializing/deserializing packed data — bit-stream cast or streaming operator.

  4. real → integer — static cast rounds; use $floor/$ceil first if you need a specific direction.

Key takeaways

  • byte/shortint/int/longint are signed by default — say 'unsigned' for counts, addresses, and bit patterns.

  • One unsigned operand makes the whole comparison unsigned — the −1 vs size() class of bug.

  • Static cast converts values, $cast checks at runtime (handles, enums), bit-stream cast reinterprets bits.

  • Assignment silently truncates from the MSB and sign-extends signed sources — know both rules cold.

Common pitfalls

  • for (int unsigned i = n; i >= 0; i--) — never terminates; unsigned i can't go below 0.

  • Assigning int to enum without $cast — compile error, or worse, an out-of-range enum via casting tricks.

  • Expecting real→int static cast to truncate — it rounds; 3.7 becomes 4.

  • Assuming narrow signed values zero-extend — sign-extension fills the high bits with 1s.