Integer Types
byte, shortint, int, longint, integer, time — signed behavior, overflow, 2-state vs 4-state.
Module 2 · Page 2.2
Not All Integers Are Created Equal
Ask a Verilog engineer what integer type they use for loop counters and they'll say integer — 32-bit, 4-state, signed. Ask a SystemVerilog engineer the same question and they'll say int — also 32-bit, signed, but 2-state. Those two types look almost identical until you hit a simulation with an uninitialized path, or try to use one in a constraint, or discover that your 30-hour regression's timestamp counter rolled over at 2³²−1.
SystemVerilog brought six integer types into the language. Four of them — byte, shortint, int, longint — are borrowed directly from C, are 2-state (no X or Z), and are signed by default. The other two — integer and time — are from Verilog's original type system, are 4-state, and behave more like hardware signals than software variables.
The practical split: use the 2-state C-style types for all testbench arithmetic, loop control, counters, and index calculations. Use time when you're working with $time and simulation timestamps. Avoid integer in new code — it's superseded by int.
Two Families — C-Style vs Verilog-Style
The mental model is simple. SystemVerilog's C-style integer types work exactly like their C counterparts: fixed width, signed by default, wrap on overflow, initialize to 0, and cannot hold X or Z. They exist primarily for testbench arithmetic where hardware unknowns are never meaningful.
The Verilog-inherited types — integer and time — carry the 4-state heritage. They initialize to X, support X and Z in operations, and behave like wider versions of logic. In practice: integer is a 32-bit version of logic signed [31:0], and time is a 64-bit version of logic [63:0] (unsigned).
- byte — 8-bit signed — Range: −128 to +127. 2-state. Use for small counters, character codes, byte-level data in TB. Equivalent to C's signed char.
- shortint — 16-bit signed — Range: −32,768 to +32,767. 2-state. Rarely needed; most engineers skip straight to int. Useful when memory footprint of large arrays matters.
- int — 32-bit signed — Range: −2.1B to +2.1B. 2-state. The default choice for loop variables and counters. C's int. Initializes to 0. Used in foreach, for loops, array indices.
- longint — 64-bit signed — Range: ±9.2×10¹⁸. 2-state. Use for simulation timestamps, cycle counters in long runs, large multiplication results, or any value that might exceed 2B.
- integer — 32-bit signed (4-state) — Legacy Verilog type. 4-state — initializes to X. Equivalent to logic signed [31:0]. Avoid in new SV code. Use int instead.
- time — 64-bit unsigned (4-state) — For storing $time values. 4-state, unsigned. Initializes to X. Use with $time, $realtime. Use longint unsigned for 2-state equivalent.
Complete Reference — Ranges, Defaults, and Rules
| Type | Width | States | Signed? | Init value | Min | Max |
|---|---|---|---|---|---|---|
byte | 8-bit | 2-state | Yes | 0 | -128 | +127 |
shortint | 16-bit | 2-state | Yes | 0 | -32,768 | +32,767 |
int | 32-bit | 2-state | Yes | 0 | -2,147,483,648 | +2,147,483,647 |
longint | 64-bit | 2-state | Yes | 0 | -9.2×10¹⁸ | +9.2×10¹⁸ |
integer | 32-bit | 4-state | Yes | X | -2,147,483,648 | +2,147,483,647 |
time | 64-bit | 4-state | No (unsigned) | X | 0 | +18.4×10¹⁸ |
// ── 2-STATE C-STYLE TYPES ─────────────────────────────────────────
byte b = 8'h41; // 'A' in ASCII — 8-bit signed
shortint si = -1000; // 16-bit signed
int i = 0; // 32-bit signed — standard loop/counter type
longint li = 0; // 64-bit signed — for timestamps, large counts
// Unsigned variants of C-style types
byte unsigned ub = 255; // 0 to 255
shortint unsigned us = 65535; // 0 to 65535
int unsigned ui = 4294967295; // 0 to 4,294,967,295
longint unsigned ul = 0; // 0 to 18.4×10^18
// ── 4-STATE LEGACY TYPES ──────────────────────────────────────────
integer legacy = 0; // 32-bit 4-state signed — avoid in new SV
time ts; // 64-bit 4-state unsigned — starts as X
ts = $time; // capture current simulation time
// ── OVERFLOW BEHAVIOR — wraps around ─────────────────────────────
byte bmax = 127;
bmax++; // bmax becomes -128 (wraps to min)
int imax = 2147483647;
imax++; // imax becomes -2147483648 (wraps)
// ── TYPE DETERMINES ARITHMETIC CONTEXT ───────────────────────────
byte x = 100, y = 100;
int result;
result = x + y; // 200 — promoted to int for arithmetic
byte bresult = x + y; // -56 — truncated back to byte (overflow!)
// ── Signed division truncates toward zero (C behavior) ───────────
int a = -7, b_div = 2;
$display("-7 / 2 = %0d", a / b_div); // -3 (truncate, not floor)Visual — Width, Ranges, and Overflow Behavior
Type Width Comparison
| Type | Bit 63 | Bit 31 | Bit 15 | Bit 7 | Bit 0 | Total bits |
|---|---|---|---|---|---|---|
byte | — | — | — | MSB (sign) | LSB | 8 |
shortint | — | — | MSB (sign) | ✓ | LSB | 16 |
int / integer | — | MSB (sign) | ✓ | ✓ | LSB | 32 |
longint / time | MSB (sign/bit63) | ✓ | ✓ | ✓ | LSB | 64 |
Signed Overflow — Where the Wrap Happens
| Type | Max value | Max + 1 wraps to | Min value | Min - 1 wraps to |
|---|---|---|---|---|
byte | 127 (0x7F) | -128 (0x80) | -128 (0x80) | 127 (0x7F) |
shortint | 32,767 (0x7FFF) | -32,768 | -32,768 | 32,767 |
int | 2,147,483,647 | -2,147,483,648 | -2,147,483,648 | 2,147,483,647 |
longint | 9.2×10¹⁸ | -9.2×10¹⁸ | -9.2×10¹⁸ | 9.2×10¹⁸ |
Signed vs Unsigned Arithmetic — Where They Diverge
| Expression | Type | Bit pattern | Interpreted as | Result |
|---|---|---|---|---|
b = 8'hFF | byte (signed) | 1111_1111 | -1 | -1 |
b = 8'hFF | byte unsigned | 1111_1111 | 255 | 255 |
-7 / 2 | int (signed) | — | Truncates toward 0 | -3 |
-7 / 2 | int unsigned | — | Treats -7 as huge positive | Large number |
127 + 1 | byte | — | Wraps to -128 | -128 |
255 + 1 | byte unsigned | — | Wraps to 0 | 0 |
Code Examples — From Loop Variables to Timestamp Handling
Example 1 — Beginner: All Integer Types and Overflow
module tb_integer_types;
byte b = 0;
shortint si = 0;
int i = 0;
longint li = 0;
integer ig; // 4-state: starts as X
time ts; // 4-state: starts as X
initial begin
// ── Initialization differences ────────────────────────────────
$display("byte init: %0d", b); // 0
$display("int init: %0d", i); // 0
$display("integer init: %0d", ig); // X
$display("time init: %0d", ts); // X
// ── byte overflow ─────────────────────────────────────────────
b = 127;
b++;
$display("byte 127+1 = %0d", b); // -128 (wraps)
b = -128;
b--;
$display("byte -128-1 = %0d", b); // 127 (wraps from min to max)
// ── int vs integer behavior ───────────────────────────────────
ig = ig + 1;
$display("X + 1 (integer) = %0d", ig); // X — arithmetic with X stays X
i = i + 1;
$display("0 + 1 (int) = %0d", i); // 1 — clean arithmetic
// ── Signed division truncates toward zero ─────────────────────
$display("-7 / 2 = %0d", -7 / 2); // -3 (not -4 like floor division)
$display("-7 %% 2 = %0d", -7 % 2); // -1 (sign follows dividend)
// ── time type for simulation timestamps ───────────────────────
ts = $time;
$display("Sim time = %0t ns", ts);
$finish;
end
endmoduleExpected output:
byte init: 0
int init: 0
integer init: x
time init: x
byte 127+1 = -128
byte -128-1 = 127
X + 1 (integer) = x
0 + 1 (int) = 1
-7 / 2 = -3
-7 % 2 = -1
Sim time = 0 nsExample 2 — Intermediate: Cycle Counter and Timestamp Tracking
module tb_cycle_counter;
logic clk = 0;
longint cycle_count = 0; // 64-bit: won't overflow in any real test
longint txn_start_cycle;
longint txn_end_cycle;
int latency; // 32-bit is enough for latency in cycles
always #5 clk = ~clk; // 10ns period
always @(posedge clk)
cycle_count++; // count every rising edge
initial begin
// Simulate sending a transaction and measuring latency
@(posedge clk); txn_start_cycle = cycle_count;
$display("Transaction sent at cycle %0d", txn_start_cycle);
// Wait 15 cycles to simulate DUT processing
repeat(15) @(posedge clk);
txn_end_cycle = cycle_count;
latency = int'(txn_end_cycle - txn_start_cycle);
$display("Response at cycle %0d, latency = %0d cycles",
txn_end_cycle, latency);
// Demonstrate why int would fail for large cycle counts
int int_count = 2147483647; // max int
longint long_count = 2147483647;
int_count++; // wraps to -2147483648!
long_count++; // 2147483648 — correct
$display("int overflow: %0d", int_count); // -2147483648
$display("longint safe: %0d", long_count); // 2147483648
$finish;
end
endmoduleExample 3 — Verification: Transaction ID Counter and Error Accumulator
class axi_scoreboard;
int total_txns = 0; // transaction count — 32-bit plenty
int pass_count = 0;
int fail_count = 0;
longint total_bytes = 0; // byte count — use longint, can exceed 2B
longint start_time; // simulation timestamp
int max_latency = 0;
int min_latency = 2147483647; // initialize to max so first real value wins
function void record_txn(logic [31:0] data, int latency_cycles, int byte_count);
total_txns++;
total_bytes += byte_count; // longint += int: safe widening
if (latency_cycles > max_latency) max_latency = latency_cycles;
if (latency_cycles < min_latency) min_latency = latency_cycles;
endfunction
function void print_summary();
longint elapsed = $time - start_time;
$display("=== Scoreboard Summary ===");
$display("Transactions: %0d", total_txns);
$display("Pass / Fail: %0d / %0d", pass_count, fail_count);
$display("Total bytes: %0d", total_bytes);
$display("Latency min/max: %0d / %0d cycles", min_latency, max_latency);
$display("Test duration: %0d ns", elapsed);
endfunction
endclass
module tb_sb_demo;
initial begin
axi_scoreboard sb = new();
sb.start_time = $time;
sb.record_txn(32'hABCD, 5, 64);
sb.record_txn(32'h1234, 12, 128);
sb.record_txn(32'hFFFF, 3, 32);
#100;
sb.print_summary();
$finish;
end
endmoduleExample 4 — Corner Case: Signed Arithmetic Traps
module tb_signed_traps;
initial begin
// ── byte is signed — max is 127, not 255 ─────────────────────
byte b = 200; // DANGEROUS: 200 > 127, assigned as -56!
$display("byte = 200 stored as: %0d", b); // -56
byte unsigned ub = 200; // OK: 200 fits in 0..255
$display("byte unsigned = 200: %0d", ub); // 200
// ── Mixing signed and unsigned in comparison ──────────────────
int si = -1;
int unsigned ui = 1;
// When int and int unsigned are compared, int is promoted to unsigned
// -1 as int = 0xFFFFFFFF as unsigned = 4294967295
if (si > ui)
$display("-1 > 1 = TRUE (signed -1 promoted to uint!)"); // prints
// Use $signed() and $unsigned() casts to control interpretation
if ($signed(si) > $signed(ui))
$display("-1 > 1 with signed context = still wrong cast");
// Correct: compare same types
int ui_as_int = int'(ui);
if (si > ui_as_int)
$display("-1 > 1 signed: TRUE (same type)"); // -1 > 1 is false
else
$display("-1 < 1 signed: correct"); // prints
// ── Widening assignment: always safe ─────────────────────────
byte small = 100;
int wide = small; // 100 sign-extended to 32-bit: 100
$display("byte 100 → int: %0d", wide); // 100
small = -10;
wide = small; // -10 sign-extended: -10
$display("byte -10 → int: %0d", wide); // -10 (sign preserved)
// ── Narrowing: may lose bits ──────────────────────────────────
int big = 300;
byte narrow = byte'(big); // explicit cast: 300 truncated to 8 bits
$display("int 300 → byte: %0d", narrow); // 44 (300 = 256+44, lower 8 bits)
$finish;
end
endmoduleExpected output:
byte = 200 stored as: -56
byte unsigned = 200: 200
-1 > 1 = TRUE (signed -1 promoted to uint!)
-1 < 1 signed: correct
byte 100 → int: 100
byte -10 → int: -10
int 300 → byte: 44Simulation Behavior — Arithmetic, Promotion, and Overflow
Arithmetic Promotion Rules
When you mix integer types in an expression, SystemVerilog follows standard promotion rules: smaller types are sign-extended to the width of the widest operand. A byte added to an int promotes the byte to int before the addition. The result is int-width. Assigning that result back to a byte truncates — potentially losing data.
The critical nuance: signed vs unsigned context propagates based on the types of the operands. Mixing a signed int with an int unsigned in a comparison promotes the signed operand to unsigned — which means -1 becomes a very large positive number. This is the same rule as C, and it causes the same class of bugs.
The 4-State integer in Arithmetic
Any arithmetic involving an X-valued integer produces X. This includes addition, subtraction, multiplication, and comparison. If your loop uses integer i instead of int i, and by some chance i is never explicitly initialized, the loop body never executes — the i < N condition evaluates to X, which is treated as false. The loop silently never runs.
| Scenario | int behavior | integer behavior |
|---|---|---|
| Initial value | 0 | X |
| Uninitialized + 1 | 1 | X |
| Loop condition uninitialized | Loop runs (0 < N) | Loop never runs (X treated as false) |
| Overflow behavior | Wraps silently at 2³²-1 | Wraps silently at 2³²-1 (when not X) |
| In constraint | Full solver support | Solver may have limitations |
Where Integer Types Belong in Real Verification
// ── 1. LOOP VARIABLES: int is the standard ────────────────────────
for (int i = 0; i < 256; i++)
$display("addr[%0d] = %h", i, mem[i]);
// ── 2. FOREACH INDEX: automatic int ──────────────────────────────
foreach (arr[i]) // i is implicitly int — no declaration needed
arr[i] = i * 2;
// ── 3. TRANSACTION COUNTER: int sufficient ────────────────────────
int txn_id = 0;
function int next_id(); return txn_id++; endfunction
// ── 4. BYTE BUFFER: byte type for memory-efficient arrays ─────────
byte packet_buf [1024]; // 1KB buffer — 1/4 of int array memory
// ── 5. SIMULATION TIMESTAMP: longint ─────────────────────────────
longint sim_start, sim_end;
// initial: sim_start = $time;
// final: sim_end = $time; $display("Total sim: %0dns", sim_end - sim_start);
// ── 6. LATENCY MEASUREMENT: int ───────────────────────────────────
longint req_time, resp_time;
int latency_ns;
// req_time = $time;
// ... wait for response ...
// resp_time = $time;
// latency_ns = int'(resp_time - req_time);
// ── 7. COVERAGE BINS: int for hit counts ──────────────────────────
int opcode_hits [256]; // how many times each opcode was seen
// opcode_hits[op]++; — simple and safe
// ── 8. CONSTRAINT: int works natively ────────────────────────────
class rand_txn;
rand int unsigned burst_len;
constraint len_c { burst_len inside {[1:256]}; }
endclassCommon Bugs — The Silent Ones Are the Worst
Bug 1 — Using integer Instead of int: Uninitialized Loop Never Runs
// BUGGY: integer is 4-state, starts as X
integer i; // i = X at start — NOT zero!
for (i = 0; i < 10; i++) // explicitly initialized: this is OK
$display("%0d", i); // works fine when initialized in for()
// The dangerous case:
integer count; // count = X
while (count < 10) // X < 10 → evaluates to X → treated as false!
count++; // NEVER EXECUTES — loop body skipped entirely
$display("count = %0d", count); // still X
// FIXED: use int (always 0) or initialize explicitly
int safe_count; // = 0 automatically
while (safe_count < 10)
safe_count++; // runs 10 times correctlyBug 2 — int Counter Overflows in Long Simulation
int cycle_cnt = 0; // BUGGY for long tests
always @(posedge clk) cycle_cnt++;
// After 2,147,483,647 cycles, cycle_cnt wraps to -2,147,483,648
// Any latency check now produces negative or garbage values
// Symptom: "latency = -2145899123 cycles" in the log
// FIXED: use longint — wraps after 9.2×10^18 cycles (never in practice)
longint safe_cycle_cnt = 0;
always @(posedge clk) safe_cycle_cnt++;Bug 3 — byte Assigned Value > 127: Silent Sign Conversion
// BUGGY: engineer thinks byte is unsigned (like hardware 8-bit)
byte status_code = 200; // 200 → stored as -56 (signed overflow)
if (status_code == 200)
$display("Got status 200"); // NEVER fires — code is -56, not 200
else if (status_code == -56)
$display("Got status -56"); // fires unexpectedly
// FIXED option 1: use byte unsigned for 0..255 range
byte unsigned status_ok = 200; // 200 stored as 200
if (status_ok == 200) $display("Got 200"); // works
// FIXED option 2: use int for general-purpose 8-bit values in TB
// Rarely worth using byte at all in TB — int is simpler and avoids confusionBug 4 — Signed/Unsigned Mixed Comparison
int signed_val = -1;
int unsigned unsigned_val = 1;
// BUGGY COMPARISON: mixing signed and unsigned
// signed_val is promoted to unsigned → -1 becomes 4294967295
if (signed_val > unsigned_val)
$display("-1 > 1 is TRUE!"); // prints — promotion made -1 = 0xFFFFFFFF
// CORRECT: compare same types or use explicit cast
if ($signed(signed_val) > $signed(unsigned_val))
$display("Signed: -1 > 1"); // false — correct behavior
else
$display("Signed: -1 < 1 — correct"); // prints
// GOLDEN RULE: never mix signed and unsigned in comparisons
// Cast explicitly or use the same signedness consistentlyInterview Questions
Beginner Level
Q1: What is the difference between int and integer in SystemVerilog? Both are 32-bit signed types. int is 2-state (0 and 1 only) and initializes to 0 — it's the C-style type introduced in SV. integer is 4-state (0, 1, X, Z) and initializes to X — it's the legacy Verilog type. In practice, use int for all new SV code. The only scenario where integer matters is in legacy Verilog RTL that you're not allowed to modify. Q2: Are the SV integer types (byte, int, longint) signed or unsigned by default? All four C-style integer types (byte, shortint, int, longint) are signed by default — matching C behavior. To get unsigned versions, declare them explicitly: int unsigned, byte unsigned, etc. Note that bit is unsigned by default, and time is unsigned (no signed variant).
Intermediate Level
Q3: A verification engineer's scoreboard starts failing after 2.1 billion transactions. The pass count shows a negative number. What type was used and how should it be fixed? The counter was declared as int, which is a 32-bit signed type with maximum value 2,147,483,647. On the next increment it wraps to −2,147,483,648. Fix: declare as longint (64-bit signed, maximum ~9.2×10¹⁸) or longint unsigned. For most practical verification scenarios, longint will never overflow. Q4: What happens when you assign the value 200 to a byte variable? The value 200 exceeds byte's maximum of 127 (signed range: −128 to +127). The value wraps: 200 in 8-bit two's complement is 8'b11001000 = −56 when interpreted as signed. So the byte stores −56, not 200. To correctly store values 0–255, use byte unsigned. This is one reason many engineers use int for general-purpose 8-bit values in testbenches — no sign confusion.
Experienced Engineer Level
Q5: In a comparison between int signed_val = -1 and int unsigned unsigned_val = 1, which is larger and why? When a signed and unsigned type of the same width are compared, the signed type is promoted to unsigned. −1 in 32-bit two's complement is 0xFFFFFFFF = 4,294,967,295 as unsigned. So -1 > 1 evaluates as TRUE. This is identical to C's undefined behavior for signed/unsigned comparison, and it causes identical bugs. The fix: always compare same-signedness types, or use explicit casts with $signed()/$unsigned() to make the intended comparison context explicit.
Best Practices — Integer Type Selection
| Use case | Recommended type | Avoid | Reason |
|---|---|---|---|
| Loop variable / array index | int | integer | 2-state, initializes to 0, no X confusion |
| Transaction / event counter | int | byte, shortint | Adequate range, standard type |
| Cycle counter / timestamp | longint | int | int overflows at 2.1B — longint never will |
| Byte-level data (0–255) | byte unsigned or logic [7:0] | byte | byte is signed — 0..127 only without qualifier |
| Simulation time | longint or time | int | Sim time can be large; time is 64-bit |
| Latency in cycles | int | longint | Latency fits in 32 bits; longint is overkill |
| Large array of small values | byte unsigned [N] | int [N] | 4× memory savings for large arrays |
| Legacy Verilog code | integer (existing only) | Mixed with int | Don't mix — pick one and be consistent |
- Always initialize integer vars — Even though int initializes to 0, be explicit in class constructors and tasks. Uninitialized state is a bug waiting to happen when code is refactored.
- Never mix signed/unsigned — In comparisons and arithmetic, keep signedness consistent. If you must mix, use $signed() or $unsigned() casts to make the intent explicit.
- Avoid integer in new code — There is no scenario in modern SV where integer is the right choice over int. In legacy RTL you can't modify, document it — don't perpetuate it.
- Cast explicitly on narrowing — When assigning a wider type to a narrower one, always use an explicit cast: byte'(int_val). Implicit narrowing is legal but silent about data loss.
Summary
Six integer types, two simple rules: use int for everyday loop variables and counters, use longint whenever the value could grow large (timestamps, byte counts, cycle counters in long regressions). The 4-state types — integer and time — are either legacy or simulation-specific. The signed default on all C-style types is the detail that burns engineers who assume "8-bit unsigned" and write byte.
intis the default integer type for all TB variables. 2-state, 32-bit signed, initializes to 0.longintfor anything that might exceed 2 billion. Cycle counters, timestamps, byte totals — use longint by default for these.- All C-style integer types are signed by default.
byteholds −128 to +127, not 0 to 255. Usebyte unsignedorlogic [7:0]for 0–255. - Never mix signed and unsigned in comparisons. The promotion rules turn −1 into the largest unsigned value — same bug as in C.
- Replace
integerwithintin all new code. The 4-state behavior ofintegeris a trap, not a feature, in testbench arithmetic.