SystemVerilog Series — SV-05b: Variables, Scope, Nets & Type Compatibility — VLSI Trainers
VLSI Trainers SV Series · 9 / 44
SystemVerilog Series · SV-05b

Variables, Scope, Nets & Type Compatibility

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.

📋 Variable Declarations

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
Initialisation timing in SV vs Verilog-2001: in Verilog-2001, a declaration initialiser ran as if from an 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.

📋 Default Initial Values

Every SV variable has a well-defined default value if no initialiser is provided.

TypeDefault initial value
4-state integral (logic, integer, reg…)‘X
2-state integral (bit, int, byte…)‘0
real, shortreal0.0
EnumerationFirst value in the enumeration
string"" (empty string)
eventA new (untriggered) event
Class handlenull
chandlenull

📋 Scope and Lifetime

SV defines four distinct combinations of scope and lifetime for data declarations:

Where declaredScopeLifetimeC analogy
Outside all modules, interfaces, tasks, functionsGlobalStatic (entire elaboration and simulation)Global C variable
Inside a module or interface, outside tasks/processes/functionsLocal to module/interfaceStatic (lifetime of the module instance)C static outside a function
Inside an automatic task, function, or blockLocal to call/activationAutomatic (stack — created on entry, destroyed on exit)C automatic variable
Inside a static task, function, or block (default)LocalStaticC static inside a function
Unnamed blocks also create scope. SV allows data to be declared in unnamed blocks as well as named ones. That data is visible to the unnamed block and any nested blocks below it — but hierarchical references cannot reach it by name.

📋 static vs automatic in Detail

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
endmodule
EDARun on EDA Playground

📋 Nets, Variables & logic

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.

Verilog-2001 rules (strict)

  • Nets: driven by continuous assignments, primitive outputs, or ports
  • Variables: driven by procedural statements only
  • Multiple continuous assignment drivers resolved by net resolution function
  • Variable cannot be continuously assigned
  • Variable cannot be written through a port (must go through net)

SV rules (relaxed)

  • All variables can be driven by one continuous assignment or by procedural statements — not both
  • All data types can be written through a port
  • Since reg no longer reflects intent, logic is the preferred keyword
  • Variables still cannot have multiple continuous drivers
logic is the new reg. The keyword reg 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.

📋 Continuous vs Procedural Assignment Rules

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:

📋 Legal and Illegal Assignment Examples

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

📋 Signal Aliasing

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 (.*);
endmodule
EDARun on EDA Playground

Alias rules

📋 Type Compatibility — Four Levels

Different SV constructs require different levels of type compatibility between their operands. SV defines four levels, from strictest to most permissive:

1
Equivalent
Types are structurally identical — same bits, same state count, same signing. Interchangeable without any conversion.
2
Assignment Compatible
Includes all equivalent types plus any pair with implicit casting rules. Conversion may truncate or round. May be one-directional.
3
Cast Compatible
Includes all assignment compatible types plus any pair that can be explicitly cast. Explicit cast operator required.
4
Type Incompatible
No implicit or explicit conversion defined. Class handles and chandles fall here — cannot be cast to or from any other type.

📋 Equivalent Types — The Eight Rules

Two types are equivalent if they match under any of these eight rules. If none apply, they are non-equivalent.

  1. Built-in types: any built-in type is equivalent to every other occurrence of itself, in any scope. int in module A is equivalent to int in module B.
  2. Simple typedefs: a typedef that renames a built-in or user-defined type is equivalent to that type within the scope of the typedef identifier. typedef bit nodebit and node are equivalent.
  3. Anonymous aggregates: an anonymous enum, struct, or union is equivalent only to variables declared in the same declaration statement.
  4. Named aggregates: a typedef for an enum, unpacked struct, unpacked union, or class is equivalent to itself and to variables declared with that typedef in the same scope.
  5. Packed/integral types: packed arrays, packed structures, and built-in integral types are equivalent if they have the same total bit count, the same state count (2 vs 4), and the same signing.
  6. Unpacked arrays: equivalent when element types are equivalent and shape is identical (same number of dimensions, same element count per dimension — not necessarily same range bounds).
  7. Signing modifiers: adding unsigned to a type that is already unsigned (or signed to a type already signed) does not make it non-equivalent.
  8. Package types: a typedef declared in a package is equivalent to itself regardless of where it is imported — package types carry their identity across compilation units.
// 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)
endmodule
EDARun on EDA Playground
Instance scope and type identity. Each module instance with a user-defined type declared inside it creates a unique type. A struct declared inside 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 Compatible

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

📋 Type Incompatible

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.

📋 Quick Reference

Lifetime and scope at a glance

KeywordStorage allocatedInitialisedVisible to
staticAt elaboration, never freedBefore first initial/always blockLocal scope + nested
automaticOn entry to scope, freed on exitOn each entryLocal scope only

Net vs variable in SV

Net (wire, wor…)Variable (logic, int…)
DriversMultiple continuous drivers OK (resolved)One continuous assignment OR procedural — not both
Written byContinuous assignments, ports, primitivesProcedural statements, or one continuous assign
Through portYesYes (SV addition)
inout portYesNo (use ref)

Type compatibility summary

LevelWhat qualifiesExample
EquivalentSame structure, bits, signing, state countbit[7:0] and typedef bit[7:0] BYTE
Assignment compatibleEquivalent + types with implicit conversionint to byte (truncation), enum to int
Cast compatibleAssignment compatible + explicit cast definedint to enum via state_t'(val)
Type incompatibleNo conversion path at allClass handle to int
Coming next: SV-06 covers Attributes — the (* *) syntax for attaching named properties to SV constructs, how tools use them for synthesis hints and lint directives, and the default attribute data type.
Data Declarations & Constants☰ SV Series IndexAttributes
Scroll to Top