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:
- An identifier — always a static prefix.
- A field select (
struct_var.field) — static prefix if the left-hand part is also a static prefix. - 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.
| Expression | Declaration context | Longest static prefix | Why |
|---|---|---|---|
| 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)
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
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
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
{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 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 type | Syntax | Applies to | Priority |
|---|---|---|---|
| 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
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 });
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
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
| Context | What {} means | Example |
|---|---|---|
| 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 .xfor safe tag-dispatched access.
<</>>), the inside set-membership operator, and the conditional operator extension for non-integral types (sections 7.16–7.20).
