Variable lifetime and scope rules, the new freedom to drive a logic from a continuous assignment, signal aliasing, and the four-level type compatibility hierarchy — equivalent, assignment compatible, cast compatible, and type incompatible.
A variable declaration consists of a data type followed by one or more instances, with an optional initialiser.
// Basic declarations shortint s1, s2[0:9]; // s1 scalar, s2 unpacked array int i = 0; // declaration with initialiser const logic flag = 1; // named constant — cannot be reassigned // const class object — the handle is fixed; members can still be written const my_class obj = new(5, 3); // Variable declaration syntax: [ const ] [ lifetime ] data_type list_of_vars ; // lifetime: static | automatic // Outside a procedural context, 'automatic' is illegal.EDARun on EDA Playground
initial block — it could cause a value-change event at time 0. In SV, static variable initialisers execute before any initial or always blocks start, so they produce no event. If you need an event at time 0, use an explicit initial block.Every SV variable has a well-defined default value if no initialiser is provided.
| Type | Default initial value |
|---|---|
4-state integral (logic, integer, reg…) | ‘X |
2-state integral (bit, int, byte…) | ‘0 |
real, shortreal | 0.0 |
| Enumeration | First value in the enumeration |
string | "" (empty string) |
event | A new (untriggered) event |
| Class handle | null |
chandle | null |
SV defines four distinct combinations of scope and lifetime for data declarations:
| Where declared | Scope | Lifetime | C analogy |
|---|---|---|---|
| Outside all modules, interfaces, tasks, functions | Global | Static (entire elaboration and simulation) | Global C variable |
| Inside a module or interface, outside tasks/processes/functions | Local to module/interface | Static (lifetime of the module instance) | C static outside a function |
| Inside an automatic task, function, or block | Local to call/activation | Automatic (stack — created on entry, destroyed on exit) | C automatic variable |
| Inside a static task, function, or block (default) | Local | Static | C static inside a function |
SV lets you override the default lifetime for individual variables within a task, function, or block — regardless of the overall scope’s default.
module msl; int st0; // static — module level initial begin int st1; // static (default inside initial) static int st2; // explicitly static automatic int auto1; // automatic override end task automatic t1(); // all variables automatic by default int auto2; // automatic (from task qualifier) static int st3; // explicit static override automatic int auto3; // redundant — already automatic endtask endmodule // Program or module-level lifetime qualifier sets the default for everything inside program automatic test; int i; // not in a procedural block — remains static task foo(int a); // all variables in foo are automatic endtask endmoduleEDARun on EDA Playground
for loop variables are always automatic, regardless of the enclosing scope’s lifetime qualifier.fork…join block encompasses all spawned processes; the enclosing scope’s lifetime includes the fork block’s lifetime.Verilog-2001 drew a strict line: nets (driven by continuous assignments or ports) and variables (driven by procedural statements) were mutually exclusive. SV relaxes this significantly.
reg no longer reflects intent, logic is the preferred keywordreg historically implied “register” (storage), but it was widely misused for combinational logic too. logic is exactly equivalent to reg — same storage semantics, same 4-state behaviour — but without the misleading name. Use logic for all new code; reg is still valid but discouraged.The key rule in SV: a variable may be driven by one continuous assignment or by one or more procedural statements — never both, never by multiple continuous assignments.
For aggregate types (structs and arrays), the rule is applied at the level of the longest static prefix being written:
force statement is neither continuous nor procedural — it is exempt from the rule.inout port. Use ref ports for shared variable access across module boundaries.struct { bit [7:0] A; bit [7:0] B; byte C; } abc; // ✓ Legal — unpacked struct: each member driven by its own method assign abc.C = sel ? 8'hBE : 8'hEF; // C driven continuously not (abc.A[0],abc.B[0]); // A,B bits via primitive always @(posedge clk) abc.B <= abc.B + 1; // B driven procedurally — different element, OK // ✗ Illegal — multiple continuous assignments to the SAME element assign abc.C = sel ? 8'hDE : 8'hED; // ERROR: C already has a continuous assign // ✗ Illegal — mixing continuous and procedural on abc.A always @(posedge clk) abc.A[7:4] <= !abc.B[7:4]; // ERROR: A also has primitive assignment // Variable driven by continuous assignment — fully legal logic vw; assign vw = vara & varb; // continuous assignment to a logic variable real circ; assign circ = 2.0 * PI * R; // continuous assignment to a real // Declaration initialiser vs continuous assignment — different things wire w = vara & varb; // continuous assignment (wire) logic v = consta & constb; // initial procedural assignment (NOT continuous)EDARun on EDA Playground
The alias statement creates a bidirectional short-circuit connection between two or more net signals. Unlike assign (which is unidirectional), aliased signals share the same physical nets — a change on either side propagates immediately in both directions.
// Byte-order swap between two 32-bit buses module byte_swap (inout wire [31:0] A, inout wire [31:0] B); alias {A[7:0],A[15:8],A[23:16],A[31:24]} = B; endmodule // Expose only LSB and MSB bytes from a 32-bit bus module byte_rip (inout wire [31:0] W, inout wire [7:0] LSB, MSB); alias W[7:0] = LSB; alias W[31:24] = MSB; endmodule // Multiple alias statements are cumulative — the following two are equivalent: // alias bus16[11:0] = low12; alias bus16[15:4] = high12; // → low12[11:4] and high12[7:0] share the same wires // Alias + .* for library cell wrapper — any of lib1_dff, lib2_dff, lib3_dff macromodule my_dff(rst, clk, d, q, q_bar); alias rst = Reset = reset = RST; alias clk = Clk = clock = CLK; `LIB_DFF my_dff (.*); endmoduleEDARun on EDA Playground
wand to a wor).Different SV constructs require different levels of type compatibility between their operands. SV defines four levels, from strictest to most permissive:
Two types are equivalent if they match under any of these eight rules. If none apply, they are non-equivalent.
int in module A is equivalent to int in module B.typedef bit node → bit and node are equivalent.unsigned to a type that is already unsigned (or signed to a type already signed) does not make it non-equivalent.// Rule 2: typedef equivalence typedef bit node; // bit and node are equivalent // Rule 3: anonymous struct — AB1 and AB2 equivalent; AB3 is NOT struct {int A; int B;} AB1, AB2; struct {int A; int B;} AB3; // different declaration — not equivalent // Rule 4: named typedef — AB1 and AB2 equivalent; AB3 is NOT typedef struct {int A; int B;} AB_t; AB_t AB1, AB2; typedef struct {int A; int B;} otherAB_t; otherAB_t AB3; // NOT equivalent to AB1/AB2 // Rule 5: packed equivalence typedef bit signed [7:0] BYTE; // equivalent to 'byte' (8-bit, 2-state, signed) // Rule 6: unpacked array shape — A, B, C are all equivalent bit [9:0] A[0:5]; // shape: 6 elements of bit[9:0] bit [1:10] B[6]; // same shape (different range notation) typedef bit [10:1] uint10; uint10 C[6:1]; // also equivalent // Cross-instance equivalence — scope matters for user-defined types package p1; typedef struct {int A;} t_1; endpackage // rule 8: always equivalent typedef struct {int A;} t_2; // rule 4: equiv within $unit module top; ... s1.v1 = s2.v1; // legal: both t_1 from package p1 (rule 8) s1.v2 = s2.v2; // legal: both t_2 from $unit (rule 4) s1.v5 = s2.v5; // ILLEGAL: t_5 declared inside each sub instance separately (rule 4) endmoduleEDARun on EDA Playground
sub is a different type in instance s1 vs s2, even if the declaration is syntactically identical. To share types between instances, declare them at compilation-unit scope or in a package.Assignment compatibility is a superset of equivalence. All equivalent types are assignment compatible. Additionally, types with implicit conversion rules defined between them are assignment compatible.
Cast compatibility is a further superset. All assignment compatible types are cast compatible. Additionally, types that can be explicitly converted using the cast operator type'(expr) are cast compatible, even if implicit conversion is not available.
state_t'(some_int) — requires explicit cast.logic vector of the same width.These are all remaining non-equivalent types with no defined implicit or explicit casting path. Attempting to assign or cast between type-incompatible types is a compile error.
null.| Keyword | Storage allocated | Initialised | Visible to |
|---|---|---|---|
| static | At elaboration, never freed | Before first initial/always block | Local scope + nested |
| automatic | On entry to scope, freed on exit | On each entry | Local scope only |
| Net (wire, wor…) | Variable (logic, int…) | |
|---|---|---|
| Drivers | Multiple continuous drivers OK (resolved) | One continuous assignment OR procedural — not both |
| Written by | Continuous assignments, ports, primitives | Procedural statements, or one continuous assign |
| Through port | Yes | Yes (SV addition) |
| inout port | Yes | No (use ref) |
| Level | What qualifies | Example |
|---|---|---|
| Equivalent | Same structure, bits, signing, state count | bit[7:0] and typedef bit[7:0] BYTE |
| Assignment compatible | Equivalent + types with implicit conversion | int to byte (truncation), enum to int |
| Cast compatible | Assignment compatible + explicit cast defined | int to enum via state_t'(val) |
| Type incompatible | No conversion path at all | Class handle to int |
(* *) syntax for attaching named properties to SV constructs, how tools use them for synthesis hints and lint directives, and the default attribute data type.