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.
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 positiveThe 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.
// 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 .
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'h00FECast selection guide
Changing width/signedness/numeric type of values — static cast, or rely on assignment rules where lint allows.
Downcasting a class handle or int→enum — $cast, and always check the return value in a function-call form.
Serializing/deserializing packed data — bit-stream cast or streaming operator.
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.