Skip to content

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.

FamilyOperators
Arithmetic+ - * / % **
Bitwise~ & | ^ ^~ / ~^ (XNOR)
Logical! && ||
Reduction&a |a ^a ~&a ~|a ~^a
Shift<< >> <<< >>>
Relational< <= > >=
Equality== != === !==
Conditionalcond ? 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.

arithmetic.v — arithmetic on sized operands
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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` squared

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

bitwise.v — bit-by-bit operations
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

logical.v — boolean reduction
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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     → 0

The & vs && Gotcha

This is the most common operator-confusion bug in Verilog.

bitwise-vs-logical.v — the canonical trap
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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, ?:).
bitwise-vs-logical-correct.v — the working pattern
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ✅ 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;
end

Reduction Operators

Apply an operator across every bit of a single vector, producing a 1-bit result.

reduction.v — N bits in, 1 bit out
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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  → 1

Reduction operators are essential in real RTL:

  • |data — "is data non-zero?" (common as busy / valid signals).
  • &data — "are all bits set?" (common as full-flags, all-ones detection).
  • ^data — parity bit (used in Ethernet, UART, DRAM ECC).
reduction-in-context.v — real RTL use
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 (<<<, >>>).

shift.v — logical vs arithmetic shifts
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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).

shift-sign-extension.v — when sign extension fires
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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).

relational.v — comparisons
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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;           // 0

Signed 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:

signed-relational-trap.v — silent unsigned promotion
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wire lt_fixed = $signed(s_neg) < $signed(u_zero);   // ✅ 1

Equality Operators

Four equality operators. The four-state-aware ones are the key Verilog distinction from C.

OperatorNameBehaviourSynthesisable?
==Logical equalityReturns x if either operand contains x or z; else 0 or 1✅ (synthesised as bitwise compare)
!=Logical inequalityInverse of ==
===Case equality4-state-aware — returns 1 if every bit matches (including X/Z); else 0❌ — testbench only
!==Case inequalityInverse of ===❌ — testbench only

The == vs === Gotcha

The 4-state-aware behaviour catches every Verilog beginner.

equality.v — the canonical X-comparison trap
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

conditional.v — the workhorse of multiplexers
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 clamp

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

concat-replicate.v — bit assembly
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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-ones

The {N{...}} replication idiom is essential for parameter-driven RTL:

parametric-mask.v — replication for parameter-driven masks
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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}};
endmodule

Operator Precedence

Verilog's precedence follows C with small extensions. The full table, highest precedence first:

PrecedenceOperatorsAssociativity
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:

precedence-trap.v — the canonical mistake
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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;        // ✅ explicit

Rule: 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.

  1. Bitwise & vs logical && — confused, produces wrong-width or always-true results.
  2. == against X/Z — returns x not 0/1, silently breaks if branches.
  3. Unsigned right shift on a signed value>> zero-fills regardless of intent; use >>> on a signed operand or $signed() cast.
  4. Unsized literal promotion1 << 8 is 32-bit 0x100; truncating to 8 bits yields 0 not 0x80.
  5. Precedence with mixed familiesa == b & c is not (a == b) & c; always parenthesise.
  6. 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.

rtl/sat_add_8b.v — saturating 8-bit adder
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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];
 
endmodule

What this exercises:

  • Arithmetic (+): 9-bit add.
  • Concatenation ({a[7], a}): sign-extension to 9 bits.
  • Equality (==): bit comparison against 1'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.