Operators & Expressions¶
Most of SystemVerilog's operators are inherited straight from Verilog and behave the way you'd expect from any C-like language. The ones worth real attention are the four-state-aware comparisons and the reduction/shift operators, since both show up constantly in testbench checking code and both have sharp edges.
Arithmetic, relational, logical¶
int a = 10, b = 3;
int sum = a + b; // 13
int quo = a / b; // 3 — integer division truncates
int rem = a % b; // 1
int pw = a ** 2; // 100 — exponentiation
if (a > b && b != 0) $display("ok"); // relational + logical
Nothing unusual here — + - * / % and ** for exponentiation, > < >= <= for relational, && || ! for logical. % and / are defined for integer types; using them on real gives you real division/modulo instead of truncation.
Four-state equality — the one that actually matters¶
Plain ==/!= treat X/Z as "unknown result" — comparing anything against an X returns X, not 1 or 0, which makes if (a == b) silently false-and-also-not-true whenever either operand contains an X. SystemVerilog gives you a case equality pair that compares bit-for-bit, X/Z included:
logic [7:0] got, exp;
if (got == exp) ... // X in either operand -> comparison is X, `if` treats that as false
if (got === exp) ... // case equality: X/Z compared literally, always resolves to 1 or 0
if (got !== exp) ... // case inequality
if (got ==? exp) ... // wildcard equality: X/Z in `exp` act as don't-care
Scoreboards must use ===, not ==
A scoreboard comparing an "expected" value against a DUT's actual output should almost always use ===/!==. If the DUT drives an X on a bug, == silently evaluates to X (falsy in an if), which either gets swallowed or misreported depending on how the surrounding code is written — either way you lose the actual failure signal. === guarantees a real match/mismatch decision every time, which is exactly what "flag an X on the bus" verification is for.
==?/!=? (wildcard equality) is the useful middle ground: treat X/Z in the right-hand operand as don't-care while still comparing real bits literally. This is what you want when checking a field against a documented "don't care" mask rather than a fully-specified expected value.
Bitwise vs. reduction operators¶
The same symbols mean different things depending on whether they're binary (two operands) or unary (one operand, prefix):
bit [7:0] x = 8'b1011_0010;
bit [7:0] y = 8'b0000_1111;
bit [7:0] bw_and = x & y; // bitwise AND, result is 8 bits: 00000010
bit parity = ^x; // reduction XOR — collapses all 8 bits into ONE bit
bit any_set = |x; // reduction OR — 1 if any bit of x is set
bit all_set = &x; // reduction AND — 1 only if every bit of x is set
Reduction XOR (^x) is the standard one-liner for computing parity over a bus in a testbench check — far cheaper to write than a for loop XOR-ing bits manually.
Shift operators — logical vs. arithmetic¶
bit [7:0] u = 8'b1000_0000;
int s = -8; // signed
u >> 1; // logical right shift: 0100_0000 — always fills with 0
s >>> 1; // arithmetic right shift: sign-extends — fills with the sign bit
<</>> are logical (always zero-fill). <<</>>> are arithmetic on the right shift (sign-extends for signed types) but behave identically to << on the left shift — there's no separate "arithmetic left shift" concept, since left-shifting doesn't need sign handling.
Concatenation and replication¶
bit [3:0] nib_hi = 4'hA, nib_lo = 4'hB;
bit [7:0] byte_val = {nib_hi, nib_lo}; // concatenation: 8'hAB
bit [7:0] padded = {4'b0, nib_hi}; // pad with literal bits
bit [7:0] repeated = {2{nib_hi}}; // replication: {nib_hi, nib_hi} = 8'hAA
Replication ({n{...}}) is the idiomatic way to build sign-extension or zero-padding without writing out repeated literals by hand, and it's used constantly when building expected-value vectors in checkers.
The conditional (ternary) operator¶
Reads as "if mode == READ then read_data else write_data". Chains left-to-right for multi-way selection, but nesting more than two or three deep hurts readability fast — a case statement (see Procedural Blocks & Control Flow) is almost always clearer once you're past a single condition.
Sizing and signedness bite silently
SystemVerilog expressions get their width and signedness from the operands, and mixing signed and unsigned operands in the same expression silently converts the signed operand to unsigned for that computation — no warning, no error. A real bug pattern: comparing a logic signed [7:0] against an unsized literal like 8'hFF (unsigned) doesn't do the signed comparison you probably intended. When it matters, cast explicitly with signed'(...) / unsigned'(...) rather than relying on inferred context.