Operators & Expressions
New assignment operators, increment/decrement, the three equality operator families, wildcard comparisons, type rules for mixed logic/bit expressions, and operator precedence — all the SV additions to Verilog-2001 operators explained with worked examples.
💡 What SV Adds to Verilog Operators
Verilog-2001 had a complete operator set for hardware description, but was missing several conveniences common in C. SystemVerilog adds them without removing anything from Verilog-2001.
Verilog-2001 — what was missing
- No compound assignment:
a = a + 1every time - No increment/decrement:
i = i + 1in every loop - Only
==/===— no wildcard comparison - No way to match X/Z bits as wildcards without
casex - Mixed
logic/bittype results undefined
SystemVerilog additions
- 13 compound assignment operators:
+=,-=,*=,/=,%=,&=,|=,^=,<<=,>>=,<<<=,>>>= - 4 increment/decrement forms:
++i,--i,i++,i-- - 2 wildcard operators:
=?=,!?= - Defined result types for mixed
logic/bit,int/integer - Assignment usable as an expression (like C)
← Assignment Operators
SystemVerilog adds twelve compound assignment operators. Each is semantically equivalent to a blocking assignment of the form a = a op b, with one important difference: the left-hand side index expression is evaluated only once.
a += b ≡ a = a + ba -= b ≡ a = a - ba *= b ≡ a = a * ba /= b ≡ a = a / ba %= b ≡ a = a % ba &= b ≡ a = a & ba |= b ≡ a = a | ba ^= b ≡ a = a ^ ba <<= b ≡ a = a << ba >>= b ≡ a = a >> ba <<<= b ≡ a = a <<< ba >>>= b ≡ a = a >>> bThe single-evaluation difference
The key advantage over writing out a[i] = a[i] + 2 is that the index expression i is evaluated exactly once. If evaluating i has a side-effect (like a function call), it only happens once.
// Standard Verilog style — i[expr] evaluated twice a[get_idx()] = a[get_idx()] + 2; // get_idx() called TWICE — may return different values! // SystemVerilog compound operator — i[expr] evaluated once a[get_idx()] += 2; // get_idx() called ONCE — safe // Common RTL and testbench patterns logic [7:0] cnt = 8'h00; cnt += 1; // increment counter cnt -= 4; // decrement by 4 cnt <<= 2; // shift left 2 bits (multiply by 4) cnt &= 8'hF0; // mask lower nibble cnt |= 8'h01; // set LSB cnt ^= 8'hFF; // bitwise invert // Signed arithmetic shift — preserves sign bit int s = -128; s >>>= 1; // s = -64 (arithmetic right shift, sign extended) s >>= 1; // s = 0x7F...C0 (logical shift, no sign extension)
<=), in event expressions (@(a += 1) is illegal), or in continuous assignment statements (assign a += b is illegal). They are purely procedural.
++ Increment & Decrement
SystemVerilog adds the C-style ++ and -- operators in both prefix and postfix forms. All four forms behave as blocking assignments.
Prefix: ++ / — (evaluate AFTER change)
int i = 5; int j = ++i; // i incremented first → i=6, j=6 int k = --i; // i decremented first → i=5, k=5 // Typical use: loop control for(int n=0; n<8; ++n) arr[n] = n;
Postfix: ++ / — (evaluate BEFORE change)
int i = 5; int j = i++; // j gets old i=5, then i=6 int k = i--; // k gets old i=6, then i=5 // Typical use: index and advance write(fifo[wr_ptr++]); // write then advance ptr data = read(rd_ptr--); // read then retreat ptr
Four forms at a glance
| Form | Name | What is returned | Effect on operand |
|---|---|---|---|
| ++i | Pre-increment | Value of i after adding 1 | i = i + 1 |
| –i | Pre-decrement | Value of i after subtracting 1 | i = i – 1 |
| i++ | Post-increment | Value of i before adding 1 | i = i + 1 |
| i– | Post-decrement | Value of i before subtracting 1 | i = i – 1 |
++/-- and read in the same expression, the result is undefined. Different simulators may produce different values:
i = 10; j = i++ + (i = i - 1);
After this,
j can be 18, 19, or 20 depending on the order the simulator evaluates the sub-expressions. Tools may warn about this. Never mix ++/-- with other reads of the same variable in one expression.
Increment/decrement on real types
// ++ and -- work on real and shortreal — increment by 1.0 real r = 3.14; r++; // r = 4.14 (increments by 1.0, not by 1 ULP) r--; // r = 3.14 shortreal sf = 0.5; sf += 1.5; // shortreal supports +=, -=, *=, /=
📄 Assignment as an Expression
In SystemVerilog, an assignment can be used as an expression (just like in C), as long as it does not contain a timing control and is enclosed in parentheses. The value returned is the value that was assigned, with the type of the left-hand side.
// Assignment in an if condition (must be parenthesised) if ((a = b)) // a gets b, then if tests whether a is non-zero b = (a += 1); // a incremented, b gets the result (a+1) // Chained assignment (C-style) a = (b = (c = 5)); // c=5, b=5, a=5 (evaluated right-to-left) // Returned type is the type of the LEFT-HAND SIDE byte x; int y; y = (x = 300); // x = byte'(300) = 44 (truncated) // y = 44 (type of x, which is byte — then widened to int)
- In an event expression:
@(a += 1)— illegal - Inside a procedural continuous assignment:
assign a = (b += c)— illegal - In any expression that is not inside a procedural statement
⚡ Operations on logic and bit Types
When you mix 4-state (logic, integer) and 2-state (bit, int) operands in one expression, SystemVerilog defines the result type precisely.
| Operand A | Operand B | Result type | Why |
|---|---|---|---|
bit | logic |
logic | 4-state wins — X/Z possible in result |
bit | bit |
bit | Both 2-state — result is 2-state |
logic | logic |
logic | Both 4-state — result is 4-state |
int | integer |
integer | 4-state (integer) wins over 2-state (int) |
// Mixed operands: result type follows the promotion rules bit [7:0] b = 8'hAA; logic [7:0] l = 8'hxx; // contains X bits bit [7:0] rb = b & b; // bit & bit → bit : rb = 8'hAA logic [7:0] rl = b & l; // bit & logic → logic: rl = 8'hxx&8'hAA = 8'h0x (mixed) // == and != with X/Z — returns X, not 0 or 1 logic cmp; cmp = (l == 8'hAA); // cmp = X (l has X bits → == returns X) // But if assigned to bit or used in if: X becomes 0 bit cmp_b = (l == 8'hAA); // cmp_b = 0 (X converted to 0 on assignment to bit) if (l == 8'hAA) ... // false (X treated as false in if)
== or != returns X because an operand contains X or Z, and that X result is used in a boolean context (if, while, ?:), the X is treated as 0 (false). This means a comparison involving X always takes the else branch — which may or may not be the correct behaviour. Use === if you need to explicitly check whether a value equals X.
& Unary Reduction Operators & Return Type
The unary reduction operators (&, ~&, |, ~|, ^, ~^) reduce a multi-bit packed expression to a single bit. The return type depends on whether the operand is 2-state or 4-state.
// 2-state operand → result type is bit int i = 32'hDEAD_BEEF; bit b = &i; // AND-reduction of int → bit (2-state result) bit p = ^i; // XOR-reduction (parity) → bit bit any = |i; // OR-reduction (any bit set?) → bit // 4-state operand → result type is logic integer j = 32'hDEAD_BEEF; logic c = &j; // AND-reduction of integer → logic (can be X) logic q = ^j; // parity → logic // Logic with X bits — reduction can produce X logic [7:0] bus = 8'b1010_x101; // one X bit logic all1 = &bus; // &: 1&1&X&... = X (unknown) logic any1 = |bus; // |: 1|0|1... = 1 (known — at least one 1) logic par = ^bus; // ^: parity with X → X // Practical: check all-zeros / any-set with reduction bit [7:0] flags; if (!|flags) // no flags set? $display("all clear"); bit parity = ^flags; // XOR parity of all 8 bits
= The Three Equality Families
SystemVerilog has three different equality operators, each handling X and Z values differently. Choosing the wrong one is one of the most common sources of simulation/synthesis discrepancies.
| Operator | Name | 0 vs 0 | 1 vs 1 | 0 vs 1 | X vs anything | Z vs anything | Result type |
|---|---|---|---|---|---|---|---|
| == | Logical equality | 1 | 1 | 0 | X | X | logic |
| != | Logical inequality | 0 | 0 | 1 | X | X | logic |
| === | Case equality (4-state) | 1 | 1 | 0 | X===X → 1 | Z===Z → 1 | bit |
| !== | Case inequality | 0 | 0 | 1 | X!==0 → 1 | Z!==0 → 1 | bit |
| =?= | Wild equality (SV new) | 1 | 1 | 0 | X on RHS → wildcard 1 | Z on RHS → wildcard 1 | bit |
| !?= | Wild inequality (SV new) | 0 | 0 | 1 | X on RHS → wildcard 0 | Z on RHS → wildcard 0 | bit |
== — can return X; X/Z operands produce unknown result.=== — always returns 0 or 1; X and Z are treated as exact bit values to match.=?= — always returns 0 or 1; X and Z in the right-hand operand act as wildcards matching any bit.
logic [3:0] a = 4'b1X10; // == : returns X because a has X bits logic eq1 = (a == 4'b1010); // X (unknown) logic eq2 = (a == 4'b1110); // X (unknown) // === : exact 4-state comparison — X and Z must match exactly bit ceq1 = (a === 4'b1X10); // 1 (exact X match) bit ceq2 = (a === 4'b1010); // 0 (X ≠ 0) // =?= : X and Z on RHS are wildcards matching any value bit weq1 = (a =?= 4'b1X10); // 1 (X on RHS matches 1 in a[2]) bit weq2 = (a =?= 4'b1Z10); // 1 (Z on RHS matches 1 in a[2]) bit weq3 = (a =?= 4'bXXXX); // 1 (all X on RHS match anything) bit weq4 = (a =?= 4'b0X10); // 0 (MSB 1≠0 — mismatch on bit 3)
❔ Wild Equality & Inequality in Depth
The =?= and !?= operators are the programmatic equivalent of casex/casez comparisons — without the synthesis risks of those statements. They are especially useful in testbench code for checking that a response matches a mask pattern where some bits are “don’t care”.
=?= wild equality
// X or Z in the RIGHT operand = wildcard // matches any value (0, 1, X, Z) in the LEFT bit r; r = (4'b1010 =?= 4'b10X0); // 1 (X matches 1) r = (4'b1010 =?= 4'b10Z0); // 1 (Z matches 1) r = (4'b1010 =?= 4'b1000); // 0 (bit2: 1≠0) r = (4'b1X10 =?= 4'b1010); // X (LHS has X, RHS is 0 — no wildcard) r = (4'b1X10 =?= 4'b1X10); // 1 (X on RHS wildcard-matches X on LHS)
!?= wild inequality
// Logical negation of =?= bit r; r = (4'b1010 !?= 4'b10X0); // 0 (wildcard matched → NOT unequal) r = (4'b1010 !?= 4'b1000); // 1 (no wildcard — 1010 ≠ 1000) r = (4'b1010 !?= 4'bXXXX); // 0 (all wildcards → always equal)
Wild equality vs casex/casez — why =?= is preferred
// casex can accidentally treat X in the SUBJECT as wildcard (simulation hazard) // =?= only treats X/Z in the RIGHT operand as wildcard // The subject (left side) is compared exactly, giving predictable simulation // Pattern: check that a DUT response matches a mask // Bits set to X in the mask are "don't-care" positions function automatic bit matches_mask( logic [7:0] actual, logic [7:0] mask // X = don't care, 0/1 = must match ); return (actual =?= mask); endfunction // Check various DUT outputs against a mask matches_mask(8'b1010_0011, 8'b10XX_00X1) // 1 — X positions match anything matches_mask(8'b1010_0001, 8'b10XX_00X1) // 1 matches_mask(8'b0010_0011, 8'b10XX_00X1) // 0 — MSB 0≠1
=?= and !?= always return 0 or 1 — never X. This is the same guarantee as ===/!==. For testbench code comparing DUT outputs, this makes the result always usable in if statements without worrying about X propagation masking a bug.
Unequal operand widths
// When operands differ in width, the shorter one is extended // using the same rules as === / !==: // — zero-extended for unsigned types // — sign-extended for signed types bit r; r = (4'b1010 =?= 6'bXX1010); // 4'b1010 zero-extends to 6'b001010 // 001010 =?= XX1010 → 1 (Xs match leading 0s)
📈 Operator Precedence Table
Operator precedence determines which operation is performed first when there are no explicit parentheses. Higher precedence binds more tightly. When two operators have equal precedence, associativity determines the order (left = left-to-right, right = right-to-left).
| Precedence | Operators | Assoc. | Category |
|---|---|---|---|
| Highest | [] :: . | left | Selection, scope, member access |
| () ! ~ & ~& | ~| ^ ~^ ^~ ++ — (unary) + – (unary) | right | Unary operators, grouping | |
| ** | left | Power | |
| * / % | left | Multiply, divide, modulo | |
| + – (binary) | left | Add, subtract | |
| << >> <<< >>> | left | Shift operators | |
| < <= > >= inside dist | left | Relational & set membership | |
| == != === !== =?= !?= | left | Equality operators (all three families) | |
| & (binary) | left | Bitwise AND | |
| ^ ~^ ^~ (binary) | left | Bitwise XOR / XNOR | |
| | (binary) | left | Bitwise OR | |
| && | left | Logical AND | |
| || | left | Logical OR | |
| ?: (conditional) | right | Ternary | |
| -> | right | Implication (constraint expressions) | |
| Lowest | = += -= *= /= %= &= |= ^= <<= >>= <<<= >>>= <= (NBA) := :/ {} {{}} | none | Assignment operators, non-blocking, concatenation |
Green = new in SystemVerilog. Green row = highest precedence. Red row = lowest.
Practical precedence gotchas
// Unary minus vs power: -2**4 = -(2**4) = -16, NOT (-2)**4 = 16 int a = -2**4; // a = -16 (** binds tighter than unary -) int b = (-2)**4; // b = 16 (parentheses force the grouping) // Bitwise vs logical: & vs && bit x = (4 & 3) == 0; // 4&3=0, 0==0=1: x=1 (& higher than ==) bit y = 4 & (3 == 0); // 3==0=0, 4&0=0: y=0 (explicit parens) // Addition before shift: a << 1 + 2 = a << 3, NOT (a<<1)+2 logic [7:0] r = 8'h01 << 1 + 2; // = 8'h01 << 3 = 8'h08 (+ before <<) // Ternary is right-associative: a?b:c?d:e = a?b:(c?d:e) int z = (1)?2:(0)?3:4; // z = 2 (first condition true)
📋 Quick Reference
Assignment operators — all 12
// Arithmetic a+=b a-=b a*=b a/=b a%=b // Bitwise a&=b a|=b a^=b // Shift a<<=b a>>=b a<<<=b a>>>=b // Inc/dec ++i --i i++ i--
Equality operator decision guide
- Use
==/!=for RTL — realistic simulation (X propagates, reveals uninitialised logic). - Use
===/!==in testbenches when you need to check whether a signal is exactly X or Z. - Use
=?=/!?=in testbenches when you need “don’t-care” bit positions — safer thancasex.
Type result rules
bit op logic→ result islogic(4-state wins).int op integer→ result isinteger(4-state wins).- Unary reduction of 2-state packed →
bit. Of 4-state packed →logic. ==/!=returnlogic(can be X).===/!==/=?=/!?=returnbit(always 0 or 1).
