SYSTEMVERILOG SERIES · SV-07D

SystemVerilog Series — SV-07d: Aggregate Expressions, Operator Overloading, Streaming & inside — VLSI Trainers
SystemVerilog Series · SV-07d

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
Aggregate comparison uses == element-by-element. For a struct, each member is compared with == 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.

Overload declaration syntax
// 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
Overloading rules:
  • 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 += B becomes A = 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
Scope and visibility: An overload declaration follows the same scope visibility rules as a data declaration — it must appear before the expressions that use it and must be in a scope visible to those expressions. The bound function is resolved using the same scope-search rules as any function call from the scope where the operator appears.

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

Syntax overview
// { 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.

Fig 1 — int j = ‘A’B’C’D’ (ASCII: A=MSB, D=LSB) — streaming operations
{>> {j}}
A
B
C
D
left-to-right order: A B C D (big-endian — no change)
{<<byte{j}}
D
C
B
A
byte slices reversed: D C B A (little-endian)
{<<16{j}}
C
D
A
B
16-bit half-word slices reversed: CD AB
{<<{8’b00110101}}
1
0
1
0
1
1
0
0
1-bit slices reversed: bit-reversal = 8’b10101100
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
Slice size and partial last slice: If the total bit count is not evenly divisible by the slice size, the last slice is smaller — no zero-padding is added. For example, {<< 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)
Pack vs unpack size rules: Pack (RHS): if destination is larger → stream is left-justified, right-padded with zeros. If destination is smaller → 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 : $];
The 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.

Type rules for ternary branches
Branch A typeBranch B typeRule
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.

Syntax
// 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 setAll comparisons resultinside 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
inside vs casez — why inside is preferred for testbench range checks: 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

FormEffect
{>> {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}} = srcUnpack 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 1 on match, 0 on no match, X if 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, if conditions, and constraint blocks.
This completes Section 7 — Operators and Expressions. The next article covers Section 8: Procedural Statements and Control Flow — new control structures (break, continue, return, do-while, final), unique/priority qualifiers for if and case, and pattern matching.

Leave a Comment

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

Scroll to Top