Data Types
Verilog's data types split into nets (wire) and variables (reg), and the compiler enforces which one goes where — assigned-in-a-procedural-block signals must be a variable, continuously-driven signals must be a net. That split is a common source of "why won't this compile" errors for exactly the reason it looks avoidable: a signal that's driven by an assign in one version of a module and driven by an always_ff in a refactored version has to have its declaration changed too. SystemVerilog's logic type removes that whole class of error.
logic: one type, either role
logic sum, cout; // usable as either a net-like or variable-like signal
logic [3:0] result;
logic can be used anywhere reg could be used, and anywhere a single-driver wire could be used — the one restriction is that a logic signal still can't have more than one driver (for genuine multi-driver nets like a tri-state bus, plain wire is still required). In practice, this means the overwhelming majority of Verilog's wire vs. reg decisions disappear: declare everything logic unless you specifically need multi-driver net behavior, and the compiler will simply refuse to compile if you've somehow used it in a way that needs a real net.
module full_adder (
input logic a, b, cin,
output logic sum, cout
);
assign sum = a ^ b ^ cin; // logic driven by assign — fine
assign cout = (a & b) | (b & cin) | (a & cin);
endmodule
This is the exact full adder from Verilog's Operators and Expressions page, with every wire/reg decision replaced by logic — same circuit, same assign statements, one less category of declaration mistake to make.
logic is still a 4-state type under the hood — it can hold 0, 1, x (unknown), or z (high-impedance), exactly like reg did. That matters for the next section.
2-state types: bit, byte, int, and friends
Testbench code frequently doesn't need x/z at all — a loop counter, an expected-value register, a randomized stimulus field never legitimately holds "unknown." SystemVerilog adds a family of 2-state types that only ever hold 0 or 1 per bit (no x/z), which is both a stricter modeling of "this is testbench data, not a hardware signal that could be undriven" and, in simulators, meaningfully faster:
| Type | Width | Signed? |
|---|---|---|
bit | 1 (or a declared vector width) | Unsigned |
byte | 8 | Signed |
shortint | 16 | Signed |
int | 32 | Signed |
longint | 64 | Signed |
bit [7:0] expected_data; // 2-state — never x or z
int loop_count; // 2-state, 32-bit signed — a natural for-loop counter
Use logic (4-state) for anything that models a real hardware signal — DUT inputs/outputs, internal RTL signals — because a real signal genuinely can be unknown (x) at reset or undriven (z) on a tri-state bus, and losing the ability to represent that would hide real bugs. Use bit/int/byte (2-state) for pure testbench bookkeeping — loop counters, expected values, randomized class fields — where x/z was never a meaningful state to begin with.
Two more 2-state types round out the numeric family, both simulation-only (never synthesizable): real (a 64-bit signed floating-point value, matching a C double) and shortreal (32-bit, matching a C float) — useful for testbench-side calculations like modeling an analog signal or computing a bit-error-rate percentage. Separately, chandle is a 2-state type that holds an opaque handle to a foreign-language (typically C) object, used almost exclusively when a testbench calls out through the DPI (Direct Programming Interface); it isn't a numeric type at all and can't be used in arithmetic.
string
SystemVerilog adds a genuine dynamically-sized string type — something plain Verilog has no equivalent for at all (Verilog can only fake short fixed-width strings by packing ASCII bytes into a wide reg):
string test_name = "full_adder_directed_test";
$display("Running test: %s", test_name);
string is simulation-only (there's no synthesizable hardware equivalent — no register holds a "string"), which is exactly the point: it's a testbench convenience for names, messages, and file paths, not something that ever needs to become a gate.
enum
Verilog has no named-constant state representation — an FSM's states are just numbers, and Verilog's FSM Coding Styles page has to use `define or parameter constants and hope every case branch uses them consistently. SystemVerilog's enum gives named states a real type, and the compiler will flag an assignment of a value the enum doesn't define:
typedef enum logic [1:0] {
IDLE = 2'b00,
START = 2'b01,
DATA = 2'b10,
STOP = 2'b11
} frame_state_e;
frame_state_e state, next_state;
Reading state in a waveform viewer now shows IDLE/START/DATA/STOP directly instead of raw 00/01/10/11 — a small thing that saves real debugging time on every FSM in a design. FSM Coding Styles's exact frame-receiver FSM is revisited later in this topic (once always_ff and always_comb are introduced) rewritten with an enum in place of its parameter constants.
typedef
typedef (used above for frame_state_e) creates a named alias for any type — not just enums:
typedef logic [7:0] byte_t;
typedef logic [31:0] word_t;
byte_t data_byte;
word_t address;
This is purely a readability/maintainability tool: if a bus width changes from 8 to 16 bits, fixing one typedef line updates every declaration that uses byte_t, instead of hunting down every logic [7:0] in the file individually.
What's next
logic, 2-state types, string, and enum cover single-valued signals. The next page covers SystemVerilog's answer to grouping multiple related fields into one named type — struct and union — starting with the natural next step for the full adder: bundling its three inputs into one structure.