SYSTEMVERILOG SERIES · SV-07C

SystemVerilog Series — SV-07c: Concatenation, Array, Struct & Tagged Union Expressions — VLSI Trainers
SystemVerilog Series · SV-07c

Static Prefixes, Concatenation, Array, Struct & Tagged Union Expressions

The longest static prefix concept, enhanced concatenation for strings, unpacked array expressions with keys, structure expressions with member/type/default keys, and tagged union creation and member access — all with worked examples.

📋 Static Prefixes

The longest static prefix of a select expression is the longest part of that expression for which a tool has a known, constant value after elaboration. This concept is used by tools for two purposes: defining implicit sensitivity lists in always_comb blocks, and determining whether multiple drivers of a net are in conflict.

Definition

An expression is a static prefix if it is:

  1. An identifier — always a static prefix.
  2. A field select (struct_var.field) — static prefix if the left-hand part is also a static prefix.
  3. An indexed select (arr[expr]) — static prefix if the array identifier part is a static prefix and the index expression is a constant expression.

The longest static prefix is the largest sub-expression that qualifies as a static prefix.

ExpressionDeclaration contextLongest static prefixWhy
m[1][i] reg [7:0] m [5:1][5:1]; integer i; m[1] m[1] is static (1 is constant). m[1][i] is not static (i is variable).
m[p][1] localparam p = 7; m[p][1] Both p and 1 are constant expressions — the entire select is static.
m[i][1] integer i; m m[i] is not static (i is variable), so the prefix stops at the bare identifier m.

Why it matters in practice

// always_comb uses the longest static prefix to build the implicit sensitivity list.
// A variable index means the WHOLE array is in the sensitivity list.
reg  [7:0] m  [5:1][5:1];
integer   i;
localparam p = 7;

always_comb out = m[1][i];
// sensitivity: m[1] (longest static prefix of m[1][i])
// → only sensitive to m[1][0..5], not all of m

always_comb out = m[p][1];
// sensitivity: m[p][1] = m[7][1] — one specific element

always_comb out = m[i][1];
// sensitivity: m — the whole array!
// because the prefix stops at m (i is variable at the first index)
Practical rule: If you want minimal sensitivity list coverage in always_comb, use constant or localparam values for array indices when possible. A variable index at any dimension forces sensitivity to the entire sub-array from that dimension upward.

{} Concatenation

Concatenation in SystemVerilog works exactly as in Verilog-2001 for packed (integral) types. Braces {} join operands into a single packed vector, and {N{val}} replicates a value N times. The result is an unsigned packed vector whose width is the sum of all operand widths.

// Basic concatenation — all Verilog-2001
logic log1, log2, log3;
{log1, log2, log3} = 3'b111;
{log1, log2, log3} = {1'b1, 1'b1, 1'b1}; // same effect

// Replication
bit [7:0] byte_val = {4{2'b10}};  // 8'b1010_1010
bit [31:0] all_ones = {32{1'b1}}; // 32'hFFFF_FFFF

// Tools warn if widths mismatch
bit [1:0] narrow = {32'b1, 32'b1}; // RHS = 64 bits, LHS = 2 bits: truncation warning
int        small  = {1'b1, 1'b1};  // RHS = 2 bits, LHS = 32 bits: size mismatch
Concatenation result is always unsigned. Even if every operand is signed, the concatenation result is an unsigned packed vector. This is consistent with Verilog-2001. If you need a signed concatenation result, cast it: signed'({a, b}).

💬 String Concatenation (New in SV)

SystemVerilog enhances {} to support concatenation of the string type. If any operand is of type string, the entire concatenation is treated as a string — all other operands are implicitly converted to string.

// String concatenation — at least one operand must be type string
string hello = "hello";
string s;

s = {hello, " ", "world"};    // s = "hello world"
s = {s, " and goodbye"};        // s = "hello world and goodbye"

// String replication — multiplier can be a non-constant variable (SV new)
int    n = 3;
string rep = {n{"boo "}};         // "boo boo boo " — n is a variable, legal!

// Unlike bit replication, string replication allows variable multiplier
// and result is NOT truncated — the destination string grows to fit

Bit concatenation rules

  • Replication count must be a constant
  • Result width = sum of all operand widths
  • If destination is narrower: truncated (warning)
  • Cannot appear on LHS of assignment with string operands

String concatenation rules

  • Replication count can be a variable
  • Result length = sum of all operand lengths
  • Destination string grows to fit — no truncation
  • Cannot appear on the LHS of an assignment
Triggering string mode: The braces enter string concatenation mode as soon as at least one operand is of type string. A string literal like "hello" alone is not type string — you need a declared string variable in the operand list, or an explicit string'("literal") cast.

📄 Unpacked Array Expressions

Braces are also used to construct unpacked array expressions. Unlike packed concatenation, each element in the braces must correspond one-for-one to an element of the destination array — the braces match the array dimensions. Each element is evaluated in the context of an assignment to the element’s type.

// Context determines whether {} is a packed concat or unpacked array expression

// PACKED context → concatenation (Verilog-2001 style)
bit [1:0] packed_bits = {32'b1, 32'b1};  // 64-bit concat into 2-bit var: warning

// UNPACKED context → array expression (SV)
// Each value evaluated in context of the element type — no size warning:
bit   unpacked_bits[1:0] = {1, 1};     // element type is bit → 1 is bit 1: no warning
int   unpacked_ints[1:0] = {1'b1, 1'b1}; // element type is int → 1'b1 is int 1: no warn

// Replication for unpacked arrays: each {N{val}} is one dimension
bit ub[1:0];
ub = {2{1'b1}};                   // same as {1'b1, 1'b1}

int n[1:2][1:3];
n = {2{{3{y}}}};                  // same as {{y,y,y},{y,y,y}}

// Outside an assignment context: must use a type cast to signal array expression
typedef int int_arr_t[1:0];
int k = int'(int_arr_t'({1,2}));  // cast required outside assignment context

// ILLEGAL: unpacked array on the left of {} in an assignment
logic [2:0] a[1:0];
logic [2:0] b, c;
// always {b,c} = a;   // ERROR: LHS {} not recognised as unpacked array expression
Left-hand side restriction: Braces on the left-hand side of an assignment are always interpreted as a packed concatenation, never as an unpacked array expression. So {b,c} = a attempts to unpack a 2-element unpacked array into a packed concatenation of two variables — which is illegal. To unpack, assign element-by-element or use streaming operators.

🔑 Array Key Notation

When initialising unpacked arrays, three key notations let you set values without listing every element in order.

index : value
Index key
Sets the element at a specific numeric index. Error to repeat the same index.
type : value
Type key
Sets all elements (or sub-arrays) whose type is equivalent to type. Last matching value wins.
default : value
Default key
Covers all elements not already set by an index or type key. Every element must be covered.
// Index key: set element at index 1 specifically
int b[1:4];
b = '{1:10, default:0};    // b[1]=10, b[2..4]=0

// Type key: set all int elements to 5, rest to 0
b = '{int:5, default:0};    // b[1..4]=5 (all ints)

// Array of structs: struct literal per element + type key inside
struct {int a; time b;} abkey[1:0];
abkey = {{a:1, b:2ns}, {int:5, time:$time}};
// abkey[1]: a=1, b=2ns
// abkey[0]: all int fields = 5, time fields = $time

// default key for all elements
int arr[0:7];
arr = '{default:99};         // all 8 elements = 99

// RULE: every element must be covered — the following is illegal:
// b = '{1:10};  // ERROR — elements 2,3,4 not covered (no default)

📌 Structure Expressions

A structure expression initialises a struct using brace notation. There are four styles: positional, member-name, type-key, and default. They can be combined. The type of the expression is inferred from the assignment context; outside that context, an explicit cast is required.

Positional style — members by declaration order

typedef struct { int x; int y; } st;
st s1;
int k = 1;

s1 = {1, 2+k};    // s1.x=1, s1.y=3  — positional, by declaration order

Member-name style — by name, any order

s1 = {x:2, y:3+k}; // s1.x=2, s1.y=4  — named, order irrelevant
s1 = {y:10, x:5};  // order swapped — still works

default key — set all members at once

s1 = {default:2};          // s1.x=2, s1.y=2  — all members to same value
s1 = {default:'0};         // all members to their zero value
s1 = {default:'1};         // all members to all-1s

Type-key style — by member type

typedef struct {
  logic [7:0]      a;
  bit               b;
  bit signed [31:0] c;
  string            s;
} sa;
sa s2;

// Type keys + default: set all bits-of-type-int to 1, strings to "", rest to 0
s2 = {int:1, default:0, string:""};
// 'int:1' matches c (bit signed [31:0] is assignment-compatible with int)
// 'string:""' matches s
// 'default:0' covers a and b (remaining fields)

// Individual member override takes precedence over type/default
s1 = {default:'1, s:""};  // all members to all-1s EXCEPT s (which is "")

🔄 Structure Expression Key Rules

Key typeSyntaxApplies toPriority
Member name { x: 5 } The specific named member at the top level of the struct only (not sub-struct members with the same name) Highest — overrides type and default
Type key { int: 1 } All fields with an equivalent type not already set by a member name key. If the same type appears more than once, the last value wins. Middle — overrides default
Default { default: 0 } All remaining members not matched by name or type. Descends recursively into nested structs and arrays. Lowest — catch-all

Nested structures — default descends recursively

struct {
  int A;
  struct { int B, C; } BC1, BC2;
} ABC, DEF;

// Named keys for each sub-struct
ABC = {A:1, BC1:{B:2, C:3}, BC2:{B:4, C:5}};
// A=1, BC1.B=2, BC1.C=3, BC2.B=4, BC2.C=5

// default descends into nested struct automatically
DEF = {default:10};
// A=10, BC1.B=10, BC1.C=10, BC2.B=10, BC2.C=10
Member name keys reach only the top level. If you have a nested struct with a member named B inside a sub-struct named BC1, writing {B: 99} in the outer struct expression is an error — there is no top-level member named B. You must write {BC1: {B:99, C:0}, BC2: {default:0}, A:0} to set it. This avoids ambiguity when different nesting levels share member names.

Structure expression outside assignment context

// Inside assignment: type inferred from LHS — no cast needed
st s1;
s1 = {1, 2};       // OK — context tells compiler this is st{x,y}

// Outside assignment: must cast to signal struct expression intent
$display(st'({1,2}));  // explicit cast required
if (s1 == st'({1,2})) // comparison: cast used to distinguish from concat
  $display("match");

🏷 Tagged Union Expressions

A tagged union stores both a value and a tag that identifies which member is currently active. The tagged keyword creates a typed value for a specific member. Unlike a plain union, reading the wrong member is caught at runtime.

Creating tagged union values

typedef union tagged {
  void  Invalid;  // tag only — no value payload
  int   Valid;    // tag + int value
} VInt;

VInt vi1, vi2;

vi1 = tagged Valid (23+34);  // tag = Valid, value = 57
vi2 = tagged Invalid;         // tag = Invalid, no value
// Note: tagged Invalid has no expression after the member name

Nested tagged unions — an instruction set example

typedef union tagged {
  struct { bit[4:0] reg1, reg2, regd; } Add;
  union tagged {
    bit [9:0] JmpU;           // unconditional jump
    struct { bit[1:0] cc; bit[9:0] addr; } JmpC; // conditional
  } Jmp;
} Instr;

Instr i1, i2;

// Create an Add instruction — struct members by position
i1 = tagged Add { 5, 4, 3 };

// Create an Add instruction — struct members by name (order irrelevant)
i1 = tagged Add { reg2:4, regd:3, reg1:5 };

// Create a Jump/Unconditional instruction
i1 = tagged Jmp (tagged JmpU 239);

// Create a Jump/Conditional — inner struct by position
i2 = tagged Jmp (tagged JmpC { 2, 83 });

// Create a Jump/Conditional — inner struct by name
i2 = tagged Jmp (tagged JmpC { cc:2, addr:83 });
Type checking is static: The compiler knows every valid member name and the type of each member’s value. Writing tagged Add { ... } is checked at compile time — only member names that actually exist in the Add struct are allowed, and the types of the values must be compatible. This catches typos and type errors before simulation.

📋 Tagged Union Member Access

Members of a tagged union are accessed using dot notation, just like a regular struct. However, the access is type-checked against the current tag at runtime. Accessing the wrong member causes a runtime error.

// Reading member values — ONLY legal if current tag matches
Instr instr = tagged Add { 5, 4, 3 };

// LEGAL (assuming instr.tag == Add):
bit[4:0] x    = instr.Add.reg1;      // x = 5
instr.Add        = { 19, 4, 3 };     // replace whole Add struct
instr.Add.reg2  = 4;                 // update one field

// RUNTIME ERROR — if instr currently has tag Jmp:
// x = instr.Add.reg1;   // Runtime error: tag is Jmp, not Add

// Pattern: use case with tagged to safely dispatch on the tag
case(instr) matches
  tagged Add .s:
    $display("ADD r%0d = r%0d + r%0d", s.regd, s.reg1, s.reg2);
  tagged Jmp (tagged JmpU .a):
    $display("JMP %0d (unconditional)", a);
  tagged Jmp (tagged JmpC .j):
    $display("JMPCC %0d if cc=%0b", j.addr, j.cc);
endcase
Uninitialised tagged union — the tag bits are undefined. A tagged union variable that has never been assigned with a tagged member_name expression has undefined tag bits. Accessing any member of such a variable may produce unpredictable results or a runtime error. Always initialise tagged unions before reading them.

Tagged union expression type requirements

The type of a tagged expression must be known from context — either the LHS type of an assignment, a cast, or from a containing expression. These three forms all work:

// 1. RHS of an assignment to a known type
VInt v = tagged Valid 5;    // VInt context known from LHS

// 2. Explicit type cast
if (v == VInt'(tagged Valid 5)) // cast provides type
  $display("match");

// 3. Function argument where the formal type is known
task process(VInt val);
endtask
process(tagged Valid 10);    // formal type VInt known

📋 Quick Reference

Brace expression disambiguation

ContextWhat {} meansExample
RHS assigned to packed/integral type Packed concatenation (Verilog-2001) bit [3:0] x = {a,b,c,d};
RHS assigned to string (any operand is string) String concatenation (SV) string s = {str1, " ", str2};
RHS assigned to unpacked array Unpacked array expression (SV) int arr[4] = {1,2,3,4};
RHS assigned to unpacked struct Structure expression (SV) st s = {x:1, y:2};
Contains type: or default: Always array/struct expression (not concat) {default:0}
Outside any assignment (expression context) Packed concat by default — use a cast for struct/array st'({1,2})

Key notation rules

  • Index key (i:v) — exact element, highest priority, cannot repeat the same index.
  • Member name key (name:v) — top-level struct field only, highest priority.
  • Type key (type:v) — all matching fields not already set, last value wins if same type repeated.
  • Default key (default:v) — catch-all for remaining elements, descends recursively into nested structs.
  • Every element/member must be covered by at least one rule.

Tagged union rules

  • Create with: tagged MemberName value. Void members: tagged MemberName (no value).
  • Type must be known from context (LHS type, cast, or formal parameter type).
  • Member access via dot: v.MemberName — runtime-checked against current tag.
  • Accessing wrong member = runtime error.
  • Uninitialised tagged union has undefined tag — always initialise before reading.
  • Use case(v) matches tagged MemberName .x for safe tag-dispatched access.
Coming next: SV-07d covers the final operator topics — aggregate expressions, operator overloading, streaming operators (<</>>), the inside set-membership operator, and the conditional operator extension for non-integral types (sections 7.16–7.20).

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top