Skip to content

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

TypeWidthStatesSigned?Init valueMinMax
byte8-bit2-stateYes0-128+127
shortint16-bit2-stateYes0-32,768+32,767
int32-bit2-stateYes0-2,147,483,648+2,147,483,647
longint64-bit2-stateYes0-9.2×10¹⁸+9.2×10¹⁸
integer32-bit4-stateYesX-2,147,483,648+2,147,483,647
time64-bit4-stateNo (unsigned)X0+18.4×10¹⁸
SystemVerilog — Integer Type Declarations
// ── 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

TypeBit 63Bit 31Bit 15Bit 7Bit 0Total bits
byteMSB (sign)LSB8
shortintMSB (sign)LSB16
int / integerMSB (sign)LSB32
longint / timeMSB (sign/bit63)LSB64

Signed Overflow — Where the Wrap Happens

TypeMax valueMax + 1 wraps toMin valueMin - 1 wraps to
byte127 (0x7F)-128 (0x80)-128 (0x80)127 (0x7F)
shortint32,767 (0x7FFF)-32,768-32,76832,767
int2,147,483,647-2,147,483,648-2,147,483,6482,147,483,647
longint9.2×10¹⁸-9.2×10¹⁸-9.2×10¹⁸9.2×10¹⁸

Signed vs Unsigned Arithmetic — Where They Diverge

ExpressionTypeBit patternInterpreted asResult
b = 8'hFFbyte (signed)1111_1111-1-1
b = 8'hFFbyte unsigned1111_1111255255
-7 / 2int (signed)Truncates toward 0-3
-7 / 2int unsignedTreats -7 as huge positiveLarge number
127 + 1byteWraps to -128-128
255 + 1byte unsignedWraps to 00

Code Examples — From Loop Variables to Timestamp Handling

Example 1 — Beginner: All Integer Types and Overflow

Example 1 — Integer Types and Overflow Behavior
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
 
endmodule

Expected output:

Simulation 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 ns

Example 2 — Intermediate: Cycle Counter and Timestamp Tracking

Example 2 — Cycle Counter and Latency Measurement
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
 
endmodule

Example 3 — Verification: Transaction ID Counter and Error Accumulator

Example 3 — Verification Scoreboard Counters
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
endmodule

Example 4 — Corner Case: Signed Arithmetic Traps

Example 4 — Signed/Unsigned Corner Cases
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
 
endmodule

Expected output:

Simulation 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: 44

Simulation 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.

Scenarioint behaviorinteger behavior
Initial value0X
Uninitialized + 11X
Loop condition uninitializedLoop runs (0 < N)Loop never runs (X treated as false)
Overflow behaviorWraps silently at 2³²-1Wraps silently at 2³²-1 (when not X)
In constraintFull solver supportSolver may have limitations

Where Integer Types Belong in Real Verification

Verification Patterns — Integer Type Selection
// ── 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]}; }
endclass

Common Bugs — The Silent Ones Are the Worst

Bug 1 — Using integer Instead of int: Uninitialized Loop Never Runs

Bug 1 — Uninitialized integer Causes Loop to Skip
// 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 correctly

Bug 2 — int Counter Overflows in Long Simulation

Bug 2 — int Cycle Counter Wraps Negative
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

Bug 3 — byte Range Confusion
// 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 confusion

Bug 4 — Signed/Unsigned Mixed Comparison

Bug 4 — -1 is Bigger Than 1 (Signed/Unsigned Promotion)
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 consistently

Interview 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 caseRecommended typeAvoidReason
Loop variable / array indexintinteger2-state, initializes to 0, no X confusion
Transaction / event counterintbyte, shortintAdequate range, standard type
Cycle counter / timestamplongintintint overflows at 2.1B — longint never will
Byte-level data (0–255)byte unsigned or logic [7:0]bytebyte is signed — 0..127 only without qualifier
Simulation timelongint or timeintSim time can be large; time is 64-bit
Latency in cyclesintlongintLatency fits in 32 bits; longint is overkill
Large array of small valuesbyte unsigned [N]int [N]4× memory savings for large arrays
Legacy Verilog codeinteger (existing only)Mixed with intDon'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.

  • int is the default integer type for all TB variables. 2-state, 32-bit signed, initializes to 0.
  • longint for 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. byte holds −128 to +127, not 0 to 255. Use byte unsigned or logic [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 integer with int in all new code. The 4-state behavior of integer is a trap, not a feature, in testbench arithmetic.