Aggregate Expressions, Operator Overloading, Streaming Operators & Set Membership
Using aggregate expressions across ports and comparisons, extending operators to user-defined types with bind, packing and unpacking bit streams with >>/<<, the extended conditional operator, and the inside set-membership operator for clean range checks.
📌 Aggregate Expressions
An aggregate expression is any expression involving an unpacked structure, unpacked union, or unpacked array. Multi-element slices of unpacked arrays also qualify. You can use aggregate expressions anywhere a single variable of the same aggregate type would be legal.
What you can do with aggregate expressions
- Copy: assign one aggregate to another of a compatible type.
- Compare: use
==or!=between two aggregates of compatible type — the comparison is element-by-element. - Pass through ports: connect an unpacked array or struct to a module input/output port.
- Pass as arguments: pass an unpacked array or struct to a task or function.
// Aggregate assignment — same as element-by-element copy typedef struct { int x; int y; } point_t; point_t p1 = {x:3, y:4}; point_t p2; p2 = p1; // aggregate assignment: p2.x=3, p2.y=4 // Aggregate comparison if (p1 == p2) $display("points are equal"); // element-by-element == // Aggregate through a port module adder (input point_t a, output point_t sum); assign sum = {x: a.x*2, y: a.y*2}; endmodule // Aggregate array slice as an expression int arr[0:7]; int sub[0:2] = arr[2:4]; // slice is an aggregate expression // For assignment, types must be assignment-compatible typedef struct { int x; int y; } other_t; // different typedef other_t q; // q = p1; // may be illegal — types are not equivalent (different typedefs) // Use explicit cast: q = other_t'(p1); if assignment-compatible
== in turn. If any member contains X, that member comparison returns X, which propagates to the overall result — so struct_a == struct_b can return X even if the non-X members all match. For testbench code where you need exact 4-state comparison, compare with === member by member, or use a custom comparison function.
🔨 Operator Overloading
SystemVerilog allows arithmetic and relational operators to be extended to work with user-defined types (like unpacked structs) that would normally not support them. The bind declaration links an operator to a function prototype.
// bind operator function return_type func_name ( arg_types ) ; bind + function float faddff (float, float);
Operators that can be overloaded
Note: == and != between same-type structs cannot be overloaded (already legal). Assignment = from a type to itself cannot be overloaded.
Complete example — custom float struct
// Define a custom 16-bit float structure typedef struct { bit sign; bit[3:0] exponent; bit[10:0] mantissa; } float; // Bind + operator to functions for different argument combinations bind + function float faddff (float, float); // float + float bind + function float faddif (int, float); // int + float bind + function float faddfi (float, int ); // float + int bind + function float faddrf (real, float); // real + float bind + function float faddfr (float, real ); // float + real bind + function float fcopyf (float); // unary + float bind + function float fcopyi (int ); // unary + int → float float A, B, C, D; // Now + works on float variables: assign A = B + C; // equivalent to: A = faddff(B, C) assign D = A + 1.0; // equivalent to: D = faddfr(A, 1.0)
Implementing a bound function
// The prototype specifies argument TYPES, not names. // The actual function can accept any compatible types. function float fcopyi (int i); float o; o.sign = i[31]; o.exponent = 0; o.mantissa = 0; return o; endfunction
- The bound function is selected by exactly matching argument types. An integral actual can be implicitly cast if there is only one matching integral prototype.
- If the operator is in a self-determined context (no expected result type), a cast must explicitly select the overload.
- If multiple expected types could match, a cast is required to disambiguate.
- Overloading does not change the operator’s behaviour for its original legal types — existing uses are unaffected.
- Compound operators like
+=are automatically derived:A += BbecomesA = A + B, using the overloaded+and the normal=.
→ Overloading Assignment & Compound Operators
Overloading the = assignment operator enables implicit casting between the custom type and other types — the same functions used for unary + can handle this.
// Overload = to enable implicit casts FROM int/real TO float bind = function float fcopyi(int); // int → float on assignment bind = function float fcopyr(real); // real → float on assignment bind = function float fcopyr(shortreal); // shortreal → float float f; f = 5; // now legal: calls fcopyi(5) f = 3.14; // now legal: calls fcopyr(3.14) // Compound operators derived automatically: float A, B; bind + function float faddff(float, float); // A += B is automatically equivalent to A = A + B // The overloaded + and the normal = are chained: always @(posedge clk) A += B; // calls faddff(A,B) then normal float=float assign
🔂 Streaming Operators — Pack / Unpack
The streaming operators {>>{...}} and {<<{...}} pack a list of variables into a bit stream (on the RHS) or unpack a stream into variables (on the LHS). The direction arrows indicate the order in which bits flow into the stream.
// { stream_operator [ slice_size ] { expressions } } // stream_operator: >> (left-to-right) or << (right-to-left) // slice_size: constant bits per slice (optional, default = 1) { >> {a, b, c} } // pack a,b,c left-to-right into a stream { << {a, b, c} } // pack a,b,c right-to-left (little-endian) { << byte {x} } // break x into bytes, stream bytes right-to-left { << 16 {x} } // break into 16-bit slices, reverse their order { << {8'b0011_0101} } // bit-reverse a constant
⇆ Stream Direction & Slice Size
The direction operators and slice size interact to produce precise byte- or word-level reordering. Understanding them visually makes them much easier to use correctly.
int j = {"A", "B", "C", "D"}; // j[31:24]='A', j[23:16]='B', j[15:8]='C', j[7:0]='D' { >> {j}} // stream: "A" "B" "C" "D" (big-endian, unchanged) { << byte {j}} // stream: "D" "C" "B" "A" (little-endian byte swap) { << 16 {j}} // stream: "C" "D" "A" "B" (half-word swap) { << {8'b0011_0101}} // 8'b1010_1100 (bit reversal) { << 4 {6'b11_0101}} // 6'b0101_11 (swap 4-bit nibbles, no pad) { >> 4 {6'b11_0101}} // 6'b1101_01 (same: >> with 4-bit slice) { << 2 {{ << {4'b1101}}}} // nested: bit-reverse 4'b1101 = 4'b1011, // then swap 2-bit pairs: 4'b1110
{<< 4 { 6'b11_0101 }} has 6 bits / 4-bit slices = one full slice (4 bits) + one 2-bit remnant. The 2-bit remnant is streamed as-is.
📄 Packing & Unpacking Examples
Basic pack operations (RHS)
int a, b, c; logic [10:0] up [3:0]; logic [11:1] p1, p2, p3, p4; // Pack a, b, c (3 × 32-bit = 96 bits) into a 96-bit vector bit [96:1] y = { >>{ a, b, c }}; // OK: exact 96-bit fit // Pack into a wider variable: right-padded with zeros bit [99:0] d = { >>{ a, b, c }}; // OK: 96 bits in 100-bit var; bits[3:0] = 0 // Errors: // int j = {>>{a,b,c}}; // ERROR: j is 32 bits but stream is 96 — too narrow // Pack unpacked array: each element streamed in reverse index order (MSB first) { >> {p1, p2, p3, p4}} = up; // p1 = up[3], p2 = up[2], p3 = up[1], p4 = up[0] (element 3 is MSB)
Basic unpack operations (LHS)
// Unpack a 23-bit value into a, b, c (total 96 bits needed): // ERROR — too few bits provided // {>>{a, b, c}} = 23'b1; // ERROR: 23 bits < 96 needed // Unpack 96 bits into a, b, c: exact fit { >>{ a, b, c }} = 96'b1; // a=0, b=0, c=1 (1 is in LSB of c) // Unpack 100 bits: 4 extra bits at the end are simply ignored { >>{ a, b, c }} = 100'b1; // OK: 4 bits left over (unread, no error)
Unpack (LHS): if source provides more bits than consumed → extra bits at the end are silently ignored. If source provides fewer bits than needed → error.
Dynamic destinations (queue, dynamic array) are resized to accept the entire stream.
🆕 Streaming Dynamic Data with with
When a stream contains variable-length fields, the with [range] expression lets you explicitly control how many elements of a dynamic array are included in the pack/unpack operation.
// Practical pattern: packet serialisation over a little-endian byte stream byte stream[$]; // byte stream queue class Packet; rand int header; rand int len; rand byte payload[]; int crc; constraint G { len > 1; payload.size() == len; } function void post_randomize(); crc = payload.sum; endfunction endclass // ── SEND: pack packet into byte stream (little-endian) ──────────────── byte q[$]; Packet p = new(); void'(p.randomize()); // Pack all fields byte-reversed into q q = { << byte { p.header, p.len, p.payload, p.crc }}; stream = {stream, q}; // append to stream // ── RECEIVE: unpack packet from byte stream ─────────────────────────── Packet r = new(); // 'with [0 +: p.len]' tells the streamer exactly how many payload bytes to unpack { << byte { r.header, r.len, r.payload with [0 +: r.len], r.crc }} = stream; // Remove consumed bytes from stream stream = stream[$bits(r) / 8 : $];
with expression is evaluated just before its array is streamed. So in the receive direction, r.payload with [0 +: r.len] — the r.len value is what was just unpacked from the stream (since r.len appears before r.payload in the unpack list). This forward-reference behaviour makes it possible to use a length field to control how many bytes of a following dynamic array are consumed — a common pattern for length-delimited packet protocols.
? Extended Conditional Operator
SystemVerilog extends the Verilog ternary ?: operator to work with non-integral types and aggregate expressions, not just integers. The rules depend on the types of the two result branches.
| Branch A type | Branch B type | Rule |
|---|---|---|
| Both integral | Both integral | Proceed as standard Verilog — mix/match sizes and signs, result is integral |
| One integral | Other can be implicitly cast to integral | Implicit cast is applied; result is integral |
| Non-integral | Non-integral | Types must be equivalent — no implicit conversion between e.g. two different struct types |
// Integral branches — standard behaviour int a = 5, b = 10; int r = (a > b) ? a : b; // r = 10 (max of a and b) // Real branch with integral promotable branch real v = (a > 3) ? 3.14 : 0; // 0 is implicitly cast to 0.0 — OK // Struct branches — both sides must have equivalent type typedef struct { int x; int y; } pt_t; pt_t p1 = {1,2}, p2 = {3,4}, res; res = (a > b) ? p1 : p2; // OK: both branches are pt_t // Array branches int arr1[4] = '{1,2,3,4}, arr2[4] = '{5,6,7,8}, arr_res[4]; arr_res = (a < b) ? arr1 : arr2; // OK: both int[4]
X/Z condition with non-integral branches
// If the ternary condition evaluates to X or Z, // both branches are evaluated and combined element-by-element. // Matching elements → returned. Non-matching → type's default uninitialised value. logic sel = 1'bx; pt_t out = sel ? p1 : p2; // p1={1,2}, p2={3,4} — sel is X // out.x: 1 != 3 → default (0) // out.y: 2 != 4 → default (0) // out = {0, 0} pt_t p3 = {1, 4}; out = sel ? p1 : p3; // p1={1,2}, p3={1,4} — sel is X // out.x: 1 == 1 → 1 (matching!) // out.y: 2 != 4 → 0 (mismatch → default) // out = {1, 0}
∈ Set Membership — inside
The inside operator tests whether a value belongs to a set. The set can contain individual values, ranges, or even arrays. It replaces chains of || comparisons and is cleaner than casez/casex for range checks.
// expression inside { member, member, [low:high], ... } // Returns: 1'b1 if match, 1'b0 if no match, 1'bx if ambiguous bit r = a inside { 5, 10, [20:30] };
Basic usage
int a = 25, b = 5, c = 10; // Test against individual values if (a inside {b, c}) // a==5 or a==10? No — a=25 // Test against ranges (inclusive) bit ba = a inside {[16:23], [32:47]}; // ba = 0 (25 not in either range) bit bb = a inside {[20:30]}; // bb = 1 (25 is in [20:30]) // Include an array — its elements are automatically traversed int arr[$] = {3, 4, 5}; if (a inside {1, 2, arr}) // same as {1, 2, 3, 4, 5} // Range using $ for type max/min bit unsigned [7:0] u8 = 200; if (u8 inside {[128:$]}) // $ = 255 for bit[7:0]: test if u8 >= 128 // String ranges — lexicographic ordering string I; if (I inside {["a rock":"hard place"]}) $display("I is between 'a rock' and 'hard place'");
inside with Z wildcards
Z values in a set member act as bit-position wildcards when comparing integral types — but only in the RHS set values, not in the LHS expression (same asymmetry as =?=).
logic [2:0] val; // Z in the set: wildcard matching while (val inside {3'b1?1}) // matches: 3'b101, 3'b111, 3'b1x1, 3'b1z1 // Z on the LHS is NOT a wildcard — it's just Z logic [2:0] lhs = 3'bz11; wire r; assign r = lhs inside {3'b1?1, 3'b011}; // lhs = z11 // Compare z11 == 1?1: bit2: z!=1 → comparison returns X → contributes X to OR // Compare z11 == 011: bit2: z!=0 → comparison returns X → contributes X to OR // Result: X (no certain match, some comparisons returned X) // r = 1'bx
inside return value rules
| LHS vs set | All comparisons result | inside returns |
|---|---|---|
| At least one set member == LHS | One is true | 1 |
| No member matches, none return X | All false | 0 |
| No member matches, but some return X | Some X, rest false | X |
casez treats Z in the case expression as wildcard, which can cause unexpected matches on uninitialised signals. inside treats Z as wildcard only in the set members (RHS), not in the LHS expression — giving more predictable behaviour. Also, inside works in any expression context; casez requires a statement.
Practical patterns
// Replace chains of || with inside // Old style: if (opcode==8'h01 || opcode==8'h02 || opcode==8'h05) do_arith(); // New style with inside: if (opcode inside {8'h01, 8'h02, 8'h05}) do_arith(); // FSM: check if state is in a subset typedef enum {IDLE,FETCH,EXEC,DONE,ERR} state_t; state_t state; if (state inside {IDLE, DONE}) // is state idle or done? accept_new_request(); // Constrained randomisation: inside is also legal in constraint blocks class Pkt; rand bit[7:0] addr; constraint c_addr { addr inside {[8'h10:8'h1F], [8'hA0:8'hAF]}; } endclass
📋 Quick Reference
Aggregate expressions — what’s legal
- Assignment between compatible aggregates (copy).
==/!=comparison (element-by-element, may return X if any element has X).- Pass through module ports or as task/function arguments.
- Types must be assignment-compatible for copy/comparison.
Operator overloading rules
bind op function ret_type func_name(arg1_type, arg2_type);— links operator to function prototype.- Overloadable:
+++---***/%==!=<<=>>==. - Argument matching is exact; one integral arg may be implicitly cast if unambiguous.
- Compound operators (
+=etc.) automatically chain the overloaded binary op with normal=. - Must appear before use in the same or enclosing scope.
Streaming operators cheatsheet
| Form | Effect |
|---|---|
| {>> {a,b,c}} | Pack a, b, c left-to-right (big-endian, no reorder) |
| {<< {a,b,c}} | Pack a, b, c right-to-left (bit-reversal across all) |
| {<< byte {x}} | Break x into bytes, reverse byte order (little-endian) |
| {<< 16 {x}} | Break x into 16-bit slices, reverse slice order |
| {>> {a}} = src | Unpack src into a (LHS unpacking) |
| expr with [i +: n] | Pack/unpack only n elements of array, starting at i |
inside operator
expr inside {v1, v2, [lo:hi], arr}— tests membership.- Returns
1on match,0on no match,Xif no match but some comparisons returned X. - Z in set values is a wildcard (unidirectional — Z in LHS is not a wildcard).
$in a range bound means the type’s maximum (or minimum) value.- Legal in expressions,
ifconditions, andconstraintblocks.
break, continue, return, do-while, final), unique/priority qualifiers for if and case, and pattern matching.
