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.
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.
An expression is a static prefix if it is:
struct_var.field) — static prefix if the left-hand part is also a static prefix.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. |
// 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 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}).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
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.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.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)
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.
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
s1 = {x:2, y:3+k}; // s1.x=2, s1.y=4 — named, order irrelevant s1 = {y:10, x:5}; // order swapped — still works
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
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 "")
| 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 |
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.// 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");
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.
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
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.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.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
| 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}) |
i:v) — exact element, highest priority, cannot repeat the same index.name:v) — top-level struct field only, highest priority.type:v) — all matching fields not already set, last value wins if same type repeated.default:v) — catch-all for remaining elements, descends recursively into nested structs.tagged MemberName value. Void members: tagged MemberName (no value).v.MemberName — runtime-checked against current tag.case(v) matches tagged MemberName .x for safe tag-dispatched access.<</>>), the inside set-membership operator, and the conditional operator extension for non-integral types (sections 7.16–7.20).