Verilog · Chapter 4.3 · Lexical Conventions
Operator Usage in Verilog
Verilog operators come in nine families: arithmetic, bitwise, logical, reduction, shift, relational, equality, conditional, and concatenation with replication. Much of the syntax is inherited from C, but the semantics are where hardware diverges from software, since these operators build real logic rather than just computing values. This lesson is the working engineer's reference for every operator, how they group by precedence, and the traps that turn into real silicon bugs. Among them are confusing a bitwise and with a logical and, using ordinary equality instead of the case-equality form when unknown or high-impedance bits are present, mixing up signed and unsigned shifts, and the promotion trap where an unsized literal quietly becomes 32 bits. By the end you will read and write Verilog expressions with confidence.
Foundation18 min readVerilogOperatorsSyntaxLexical
Chapter 4 · Page 4.3 · Lexical Conventions
The Nine Operator Families
Per IEEE 1364-2005 §5, Verilog has nine families of operators. Every line of RTL you'll ever read uses some subset of them.
| Family | Operators |
|---|---|
| Arithmetic | + - * / % ** |
| Bitwise | ~ & | ^ ^~ / ~^ (XNOR) |
| Logical | ! && || |
| Reduction | &a |a ^a ~&a ~|a ~^a |
| Shift | << >> <<< >>> |
| Relational | < <= > >= |
| Equality | == != === !== |
| Conditional | cond ? a : b |
| Concatenation / Replication | {a, b, c} {N{a}} |
Arithmetic Operators
Standard arithmetic on integer operands. Verilog widens operands to the larger of the two and performs the operation in that width.
wire [7:0] a = 8'd100;
wire [7:0] b = 8'd200;
wire [7:0] sum = a + b; // 100 + 200 = 300 → truncated to 8 bits → 44
wire [8:0] sum_w = a + b; // 9-bit dest → 300 fits → result = 9'd300
wire [7:0] diff = a - b; // 100 - 200 = -100 → as 2's complement 8-bit → 156
wire [15:0] prod = a * b; // 100 * 200 = 20 000 → fits in 16 bits
wire [7:0] quot = a / b; // integer division — truncates toward zero
wire [7:0] rem = a % b; // modulo — sign of result follows the dividend
wire [15:0] power = a ** 2; // exponentiation — `a` squaredSynthesis-critical: * and ** synthesise to multipliers (DW_mult_pipe, vendor DSP slices). / and % are not synthesisable on most flows — synthesise to dividers that produce massive area and multi-cycle paths. Use shifts (>> 2 for divide-by-4) or hardcoded constants instead.
Bitwise Operators
Operate bit-by-bit on every bit of the operand. Result width = operand width.
wire [3:0] a = 4'b1100;
wire [3:0] b = 4'b1010;
wire [3:0] band = a & b; // 4'b1000
wire [3:0] bor = a | b; // 4'b1110
wire [3:0] bxor = a ^ b; // 4'b0110
wire [3:0] bnot = ~a; // 4'b0011
wire [3:0] bxnor = a ~^ b; // 4'b1001 (equivalent: a ^~ b)Bitwise operators synthesise directly to gate networks — & becomes AND gates, | becomes OR gates, etc.
Logical Operators
Reduce each operand to a 1-bit boolean (non-zero → 1, zero → 0), then operate. Result is always 1 bit.
wire [3:0] a = 4'b1100;
wire [3:0] b = 4'b0000;
wire land = a && b; // (a != 0) AND (b != 0) → 1 && 0 → 0
wire lor = a || b; // (a != 0) OR (b != 0) → 1 || 0 → 1
wire lnot = !a; // NOT (a != 0) → !1 → 0The & vs && Gotcha
This is the most common operator-confusion bug in Verilog.
wire [3:0] a = 4'b1100;
wire [3:0] b = 4'b0011;
wire [3:0] bw = a & b; // BITWISE: bit-by-bit AND → 4'b0000
wire lg = a && b; // LOGICAL: (a!=0) && (b!=0) → 1 && 1 → 1
// Wait — `lg` is 1 even though `a & b` is 0? Yes. They mean different things.
// `&` answers "what's the bit-by-bit AND of these vectors?"
// `&&` answers "are both operands non-zero?"Rule:
&(single) — bitwise, operates on every bit of the vector, result is same width as operands. Use in datapath logic.&&(double) — logical, reduces both operands to boolean and returns a 1-bit boolean. Use in conditional expressions (if,while,?:).
// ✅ datapath — bitwise
assign masked_data = data & mask; // & — bitwise
// ✅ conditional — logical
always @(posedge clk) begin
if (en && !rst_n) // && — logical
count_q <= count_q + 1'b1;
endReduction Operators
Apply an operator across every bit of a single vector, producing a 1-bit result.
wire [7:0] data = 8'b1010_1100;
wire all_set = &data; // 1 if EVERY bit is 1 → 0 (data has 0s)
wire any_set = |data; // 1 if ANY bit is 1 → 1
wire parity = ^data; // XOR of every bit → 0 (4 ones — even parity)
wire none_set = ~|data; // NOR — 1 if every bit is 0 → 0
wire all_zero_partner = ~&data; // NAND — 1 if any bit is 0 → 1
wire even_parity_xnor = ~^data; // XNOR — 1 if even parity → 1Reduction operators are essential in real RTL:
|data— "is data non-zero?" (common asbusy/validsignals).&data— "are all bits set?" (common as full-flags, all-ones detection).^data— parity bit (used in Ethernet, UART, DRAM ECC).
// FIFO full when every bit of the count register is 1
assign full = &count_q;
// Generate parity bit for outgoing data
assign parity_bit = ^data_out;
// Interrupt asserted when any source is pending
assign irq = |source_pending;Shift Operators
Verilog has four shift operators: two logical (<<, >>) and two arithmetic (<<<, >>>).
wire signed [7:0] s = -8'sd16; // 8'b1111_0000 (two's complement -16)
wire [7:0] u = 8'b1111_0000;
wire [7:0] sl = u << 2; // logical left → 8'b1100_0000 (shifts in zeros)
wire [7:0] sr = u >> 2; // logical right → 8'b0011_1100 (shifts in zeros)
wire [7:0] al = s <<< 2; // arithmetic left → 8'b1100_0000 (same as <<)
wire [7:0] ar = s >>> 2; // arithmetic right → 8'b1111_1100 (shifts in SIGN BIT)The Sign-Extension Gotcha
>>> preserves the sign of a signed operand by replicating the MSB. On an unsigned operand, >>> behaves identically to >> (zero-fill).
wire [7:0] u_data = 8'b1111_0000;
wire signed [7:0] s_data = 8'sb1111_0000; // -16 as signed
wire [7:0] u_arith_right = u_data >>> 2; // 8'b0011_1100 — zero-fill (unsigned)
wire [7:0] s_arith_right = s_data >>> 2; // 8'b1111_1100 — sign-fill (signed)Rule: if you need sign-preserving right-shift, the operand must be declared signed (or use the $signed() cast). A wire [7:0] x; x >>> 2; produces zero-fill regardless of what you intended — because x is unsigned by default.
Relational Operators
Standard < <= > >= comparisons. Result is 1 bit (0 or 1).
wire [7:0] a = 8'd100;
wire [7:0] b = 8'd200;
wire lt = a < b; // 1
wire le = a <= b; // 1
wire gt = a > b; // 0
wire ge = a >= b; // 0Signed vs unsigned: if both operands are signed, the comparison is signed. If either is unsigned, the comparison is unsigned. This produces a real silicon trap:
wire signed [7:0] s_neg = -8'sd1; // 8'b1111_1111 as signed = -1
wire [7:0] u_zero = 8'd0;
wire lt = s_neg < u_zero; // ❌ Expected: 1 (-1 < 0). Got: 0.
// Why: `u_zero` is unsigned; the compare is
// unsigned; s_neg is reinterpreted as 8'd255.
// 255 < 0 → 0. Silent bug.The fix: declare both operands signed, or use $signed() to cast at the comparison site:
wire lt_fixed = $signed(s_neg) < $signed(u_zero); // ✅ 1Equality Operators
Four equality operators. The four-state-aware ones are the key Verilog distinction from C.
| Operator | Name | Behaviour | Synthesisable? |
|---|---|---|---|
== | Logical equality | Returns x if either operand contains x or z; else 0 or 1 | ✅ (synthesised as bitwise compare) |
!= | Logical inequality | Inverse of == | ✅ |
=== | Case equality | 4-state-aware — returns 1 if every bit matches (including X/Z); else 0 | ❌ — testbench only |
!== | Case inequality | Inverse of === | ❌ — testbench only |
The == vs === Gotcha
The 4-state-aware behaviour catches every Verilog beginner.
wire [3:0] q = 4'bxxxx; // unknown
wire [3:0] z = 4'b0000;
wire eq2 = (q == z); // ❌ Returns `x`, not `0` or `1`.
// In an `if (q == z)` this is treated as FALSE.
// So the branch is taken when q is X.
wire eq4 = (q === z); // ✅ Returns `0` (every bit differs because q has X).
wire isx = (q === 4'bxxxx); // ✅ Returns `1` — q's bits ARE all X.
// Testbench rule: when checking for X, use ===
// RTL rule: never use === — it's testbench-only.In RTL == is fine because synthesis treats X as don't-care (the synthesised hardware doesn't have X — only 0s and 1s). In testbenches, when you specifically want to check whether a signal is X, === is the only correct operator.
Conditional Operator
The single ternary operator — condition ? value_if_true : value_if_false.
wire [7:0] mux2 = sel ? a : b; // 2:1 mux
wire [7:0] mux4 = sel[1] ? (sel[0] ? d : c)
: (sel[0] ? b : a); // 4:1 mux as nested ternaries
wire [7:0] absolute = (data[7]) ? -data : data; // absolute value
wire [7:0] saturate = (sum > MAX) ? MAX : sum; // saturating clampThe ternary is the cleanest way to write a 2:1 multiplexer in RTL. Synthesisable; produces a mux cell.
Concatenation and Replication
Two operators for building wider vectors from narrower ones.
wire [3:0] a = 4'h5;
wire [3:0] b = 4'hA;
// Concatenation — `{}` joins values left-to-right, MSB first
wire [7:0] ab = {a, b}; // 8'h5A (a is upper, b is lower)
wire [11:0] abc = {4'h0, a, b}; // 12'h05A (zero-pad)
wire [11:0] sign_ext = {{4{a[3]}}, a, b}; // 12'hF5A (sign-extend a to 12 bits)
// Replication — `{N{value}}` repeats value N times
wire [15:0] all_ones = {16{1'b1}}; // 16'hFFFF
wire [15:0] all_zeros = {16{1'b0}}; // 16'h0000
wire [15:0] pattern = {4{4'hA}}; // 16'hAAAA
wire [3:0] mask = {WIDTH{1'b1}}; // parameter-driven all-onesThe {N{...}} replication idiom is essential for parameter-driven RTL:
module masked_bus #(parameter WIDTH = 8) (
input wire [WIDTH-1:0] data_in,
input wire en,
output wire [WIDTH-1:0] data_out
);
// Mask data when not enabled — `en` is replicated WIDTH times
assign data_out = data_in & {WIDTH{en}};
endmoduleOperator Precedence
Verilog's precedence follows C with small extensions. The full table, highest precedence first:
| Precedence | Operators | Associativity |
|---|---|---|
| 1 (highest) | +a -a !a ~a &a ~&a |a ~|a ^a ~^a (unary / reduction) | right-to-left |
| 2 | ** (exponentiation) | left-to-right |
| 3 | * / % | left-to-right |
| 4 | + - (binary) | left-to-right |
| 5 | << >> <<< >>> | left-to-right |
| 6 | < <= > >= | left-to-right |
| 7 | == != === !== | left-to-right |
| 8 | & (binary bitwise) | left-to-right |
| 9 | ^ ~^ (binary bitwise) | left-to-right |
| 10 | | (binary bitwise) | left-to-right |
| 11 | && (logical) | left-to-right |
| 12 | || (logical) | left-to-right |
| 13 (lowest) | ? : (conditional) | right-to-left |
The Precedence Trap
The single most common precedence bug:
// ❌ Expected: y = a AND b OR c → reader expects `(a & b) | c`
// Actual: Verilog evaluates as `a & (b | c)` because `|` is LOWER than `&` — wait no.
// Actually `|` is LOWER precedence than `&`, so `a & b | c` parses as `(a & b) | c`. ✅
// The actual trap is with relational operators:
wire bad = a + b > c; // Parses as: (a + b) > c ✅ (relational lower than arithmetic)
// But:
wire really_bad = a == b & c; // Parses as: a == (b & c)
// — because `&` is HIGHER than `==`.
// Reader probably expected: (a == b) & c.
// Always parenthesise mixed-family expressions:
wire fixed = (a == b) & c; // ✅ explicitRule: when mixing operator families, always parenthesise. The cost of an extra () is zero; the cost of a debug session is hours.
Common Mistakes
Six operator-related production bugs.
- Bitwise
&vs logical&&— confused, produces wrong-width or always-true results. ==against X/Z — returnsxnot0/1, silently breaksifbranches.- Unsigned right shift on a signed value —
>>zero-fills regardless of intent; use>>>on asignedoperand or$signed()cast. - Unsized literal promotion —
1 << 8is 32-bit0x100; truncating to 8 bits yields0not0x80. - Precedence with mixed families —
a == b & cis not(a == b) & c; always parenthesise. - Using
/and%in RTL — produces multi-cycle dividers; use shifts for power-of-2.
Worked Example — Saturating 8-bit Adder
A 1-page RTL block that exercises a representative selection of the operator catalogue.
module sat_add_8b (
input wire signed [7:0] a,
input wire signed [7:0] b,
output wire signed [7:0] sum_sat // saturates to ±127 on overflow
);
// 9-bit add preserves the sign bit through the operation
wire signed [8:0] sum_ext = {a[7], a} + {b[7], b};
// Overflow detected when bits [8] and [7] differ
wire overflow_pos = (sum_ext[8] == 1'b0) && (sum_ext[7] == 1'b1);
wire overflow_neg = (sum_ext[8] == 1'b1) && (sum_ext[7] == 1'b0);
// Saturating mux — ternary cascade
assign sum_sat = overflow_pos ? 8'sd127
: overflow_neg ? -8'sd128
: sum_ext[7:0];
endmoduleWhat this exercises:
- Arithmetic (
+): 9-bit add. - Concatenation (
{a[7], a}): sign-extension to 9 bits. - Equality (
==): bit comparison against1'b0/1'b1. - Logical (
&&): combining two overflow conditions. - Conditional (
? :): saturating mux as ternary cascade. - Signed arithmetic (
signed [7:0]): two's-complement preservation.
Roughly 25 standard cells after synthesis. Critical path: input → 9-bit adder → overflow XOR → output mux → output pin.
Interview Insights
Summary
Verilog's nine operator families cover the entire datapath surface — arithmetic for adders, bitwise for gates, reduction for parity / busy / all-ones detection, shift for power-of-2 divides and bit-position adjustment, relational + equality for comparisons, conditional for muxes, concatenation + replication for bit assembly. The semantics are mostly inherited from C; the four hardware-specific divergences (& vs &&, == vs === against X/Z, signed vs unsigned shift, unsized literal promotion) are the production-bug surface every RTL engineer learns to navigate. The next sub-topic covers number representation in depth — exactly how Verilog sized literals, base specifiers, signed conversions, and underscore-readability all interact.
Beat coverage notes: all required beats present. 🚀 RTL Design Insight callout folded into the saturating-adder worked example; 🏗 Synthesis Concern + 🔍 Debugging Insight present at key trap points. ⏱ Timing Observation, 🧠 Simulator Scheduling Insight omitted — operators are pure-lexical-then-functional; no timing or scheduling angle.
Related Tutorials
- Lexical Conventions — Chapter 4 overview; the parent layer.
- White Space Requirements — Chapter 4.1; sibling.
- Comment Implementation — Chapter 4.2; sibling.
- RTL Designing — Chapter 3; where these operators get composed into real RTL.