Verilog Operators Explained with Examples

Every expression you write in Verilog becomes hardware: an adder, a mux, a wire crossing, a tree of gates. Operators are the vocabulary of that translation, and most beginner RTL bugs are really operator bugs — a sum that silently drops its carry, a & written where && was meant, an if that never fires because an X turned the comparison into X. This chapter covers every operator family in Verilog-2005 with runnable code, and finishes with the precedence table you should keep bookmarked.

The seven operator families

FamilyOperatorsResult width
Arithmetic+ - * / % **Width of widest operand (in a self-determined context)
Relational< <= > >=1 bit
Equality== != === !==1 bit
Logical&& || !1 bit
Bitwise& | ^ ~ ~^Same as operands, lane by lane
Reduction& ~& | ~| ^ ~^ (unary)1 bit from one vector
Shift / select / build<< >> <<< >>> ?: {} {{}}Depends on operator

Notice that & appears twice. Whether it means bitwise AND or reduction AND depends only on whether it sits between two operands or in front of one. That double duty is a favourite interview trap, and we will simulate it below.

Arithmetic and relational operators

Verilog gives you +, -, *, / (integer division), % (modulus) and ** (power). The operators themselves are simple; the width rules around them are where designs break. When you assign a + b into a target, the addition is evaluated at the width of the widest signal involved — including the target — and anything beyond the destination width is thrown away without a warning. An 8-bit adder genuinely produces a 9-bit answer, so the destination must have room for the carry.

Relational operators compare magnitudes and return a single bit. One subtlety worth memorising now: if both operands are unsigned (the default for reg and wire), the comparison is unsigned. A pattern like 8'hFF is 255, not −1, unless you declared the signal signed.

module arith_ops_tb;
  reg  [7:0] a, b;
  reg  [7:0] sum;        // same width as operands
  reg  [8:0] sum_wide;   // one extra bit for the carry

  initial begin
    a = 8'd200; b = 8'd100;

    sum      = a + b;    // 300 does not fit in 8 bits
    sum_wide = a + b;    // 9-bit result keeps the carry
    $display("a=%0d b=%0d", a, b);
    $display("8-bit sum  = %0d (truncated, 300 - 256 = 44)", sum);
    $display("9-bit sum  = %0d (carry preserved)", sum_wide);

    $display("a - b = %0d", a - b);
    $display("a * b = %0d (needs 16 bits, here truncated to 8)", a * b);
    $display("a / b = %0d (integer division, remainder dropped)", a / b);
    $display("a %% b = %0d (modulus: remainder of a/b)", a % b);

    // relational operators return 1-bit 1 or 0
    $display("a >  b : %b", a > b);
    $display("a <= b : %b", a <= b);
    $finish;
  end
endmodule
a=200 b=100
8-bit sum  = 44 (truncated, 300 - 256 = 44)
9-bit sum  = 300 (carry preserved)
a - b = 100
a * b = 32 (needs 16 bits, here truncated to 8)
a / b = 2 (integer division, remainder dropped)
a % b = 0 (modulus: remainder of a/b)
a >  b : 1
a <= b : 0
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpou08t9fq/arith_ops_tb.v:23: $finish called at 0 (1s)

The takeaway: 200 + 100 in an 8-bit register is 44, because 300 wraps modulo 256. Multiplication is even harsher — an N×N multiply needs 2N bits, so size the destination before you write the expression, not after the waveform looks wrong. Also remember that / and % synthesize into large, slow logic (unless the divisor is a power of two), so in real datapaths they are usually replaced by shifts or dedicated divider IP.

Equality: == versus ===

Verilog signals carry four values: 0, 1, X (unknown) and Z (high impedance). That forces two different ideas of “equal”.

  • Logical equality == / != asks “are these the same value?” If any compared bit is X or Z, the honest answer is “cannot tell”, so the whole result becomes X. Synthesizable — this is the equality hardware comparators implement.
  • Case equality === / !== asks “are these the same bit pattern, X and Z included?” It always returns a clean 0 or 1. It is a simulation-only construct: silicon has no way to test for X, so synthesis tools either reject it or quietly treat it as ==.
module case_eq_tb;
  reg [3:0] bus_a, bus_b;

  initial begin
    bus_a = 4'b10x1;   // bit 1 is unknown
    bus_b = 4'b10x1;   // same pattern, same X position

    // == cannot decide when an X is involved -> result is X
    $display("bus_a == bus_b : %b  (logical equality gives X)",
             bus_a == bus_b);

    // === compares literally, bit for bit, X and Z included
    $display("bus_a === bus_b: %b  (case equality matches the X too)",
             bus_a === bus_b);

    bus_b = 4'b1001;   // replace the X with a 0
    $display("bus_a == bus_b : %b  (still X: the X bit is undecidable)",
             bus_a == bus_b);
    $display("bus_a === bus_b: %b  (0: X and 0 are literally different)",
             bus_a === bus_b);

    // with clean 0/1 values the two behave identically
    bus_a = 4'b1001;
    $display("clean values   : ==%b ===%b",
             bus_a == bus_b, bus_a === bus_b);
    $finish;
  end
endmodule
bus_a == bus_b : x  (logical equality gives X)
bus_a === bus_b: 1  (case equality matches the X too)
bus_a == bus_b : x  (still X: the X bit is undecidable)
bus_a === bus_b: 0  (0: X and 0 are literally different)
clean values   : ==1 ===1
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpgop56j7c/case_eq_tb.v:26: $finish called at 0 (1s)

Why this matters practically: if (sig == 1'bx) can never be true, because the comparison itself evaluates to X and an X condition takes the else-path. To check for an unknown in a testbench, you must write if (sig === 1'bx). Keep === and !== inside testbenches and assertions; keep == and != in RTL.

Bitwise, logical and reduction operators

Three families share symbols, so learn them by what they consume and produce:

  • Bitwise (&, |, ^, ~) takes two vectors and works lane by lane: bit 3 of the result depends only on bit 3 of each input. The result is as wide as the operands. This is an array of gates.
  • Logical (&&, ||, !) collapses each whole operand to true (any bit is 1) or false (all bits 0) and returns one bit. This is control-flow logic for conditions.
  • Reduction (unary &, |, ^ and their negated forms ~&, ~|, ~^) takes a single vector and folds the operator across all its bits, producing one bit. &bus is “all ones?”, |bus is “any one?”, and ^bus is the parity — one XOR tree in a single character.
module bit_logic_red_tb;
  reg [3:0] p, q;

  initial begin
    p = 4'b1100; q = 4'b1010;

    // bitwise: works lane by lane, result is 4 bits
    $display("p & q  = %b (bitwise AND)",  p & q);
    $display("p | q  = %b (bitwise OR)",   p | q);
    $display("p ^ q  = %b (bitwise XOR)",  p ^ q);
    $display("~p     = %b (bitwise NOT)",  ~p);

    // logical: treats each whole operand as true/false, result is 1 bit
    $display("p && q = %b (both non-zero, so true)", p && q);
    $display("p || q = %b", p || q);
    $display("!p     = %b (p is non-zero, so NOT p is false)", !p);

    // reduction: folds ONE vector down to a single bit
    $display("&p = %b (1 only if every bit of p is 1)", &p);
    $display("|p = %b (1 if any bit of p is 1)", |p);
    $display("^p = %b (XOR of all bits = parity of p)", ^p);
    $finish;
  end
endmodule
p & q  = 1000 (bitwise AND)
p | q  = 1110 (bitwise OR)
p ^ q  = 0110 (bitwise XOR)
~p     = 0011 (bitwise NOT)
p && q = 1 (both non-zero, so true)
p || q = 1
!p     = 0 (p is non-zero, so NOT p is false)
&p = 0 (1 only if every bit of p is 1)
|p = 1 (1 if any bit of p is 1)
^p = 0 (XOR of all bits = parity of p)
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp0azx9xja/bit_logic_red_tb.v:22: $finish called at 0 (1s)

Study the AND trio in the log: p & q is 1000, p && q is 1, and &p is 0. Same character, three circuits. A classic review comment is if (enable & ready) — it happens to work for 1-bit signals, but the moment one operand becomes a vector the bitwise AND produces a multi-bit result and the condition means “any bit set”, which may not be what the author intended. Use && in conditions, & in datapaths.

Shift, conditional, concatenation and replication

The logical shifts << and >> move bits and fill the vacated positions with zeros. Shifting left by k multiplies by 2k (until bits fall off the top); shifting right divides. The conditional operator sel ? a : b is a 2-to-1 multiplexer in expression form — it is the only three-operand operator in the language and the standard way to describe a mux in a continuous assignment. Concatenation {a, b, c} glues fields into one bus, left operand landing in the most significant position, and replication {N{pattern}} repeats a pattern N times — the idiom behind manual sign extension.

module shift_concat_tb;
  reg [7:0]  d;
  reg [15:0] word;
  reg [3:0]  hi, lo;

  initial begin
    d = 8'b0001_0110;   // 22

    $display("d       = %b (%0d)", d, d);
    $display("d << 2  = %b (%0d)  multiply by 4", d << 2, d << 2);
    $display("d >> 1  = %b (%0d)  divide by 2",   d >> 1, d >> 1);

    // conditional operator: a 2-to-1 mux in one line
    $display("max(200,45) = %0d", (8'd200 > 8'd45) ? 8'd200 : 8'd45);

    // concatenation: glue fields into one bus
    hi = 4'hA; lo = 4'h5;
    word = {hi, lo, 8'h3C};
    $display("word = {hi,lo,8'h3C} = %h", word);

    // replication: repeat a pattern N times
    $display("{4{2'b10}}   = %b", {4{2'b10}});
    $display("sign-extend d to 16 bits: %b", {{8{d[7]}}, d});
    $finish;
  end
endmodule
d       = 00010110 (22)
d << 2  = 01011000 (88)  multiply by 4
d >> 1  = 00001011 (11)  divide by 2
max(200,45) = 200
word = {hi,lo,8'h3C} = a53c
{4{2'b10}}   = 10101010
sign-extend d to 16 bits: 0000000000010110
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp2oa5rq7a/shift_concat_tb.v:24: $finish called at 0 (1s)

For signed values, Verilog-2005 also provides the arithmetic shifts <<< and >>>. Arithmetic right shift replicates the sign bit instead of shifting in zeros, so a negative number stays negative — but only when the operand is declared signed:

reg signed [7:0] s;
s = -8'sd16;        // 1111_0000
y = s >>> 2;        // 1111_1100 = -4 (sign bit copied in)
y = s >>  2;        // 0011_1100 = 60 (zeros shifted in: wrong for signed data)

Operator precedence

From highest (binds tightest) to lowest. Operators on the same row have equal precedence and associate left to right, except ?: which associates right to left.

RankOperatorsFamily
1 (highest)! ~ & ~& | ~| ^ ~^ (unary), unary + -Negations, reductions, sign
2**Power
3* / %Multiply, divide, modulus
4+ - (binary)Add, subtract
5<< >> <<< >>>Shifts
6< <= > >=Relational
7== != === !==Equality
8&Bitwise AND
9^ ~^Bitwise XOR / XNOR
10|Bitwise OR
11&&Logical AND
12||Logical OR
13 (lowest)?:Conditional

Two traps hide in this table. First, bitwise operators bind looser than equality: a & b == c parses as a & (b == c), almost never what you meant. Second, shifts bind looser than addition: x << 1 + 1 shifts by two, not “shift then add”. Concatenation braces are self-delimiting, so they never enter precedence battles — one more reason the house style at most VLSI teams is simple: parenthesize anything with two different operator families in it.

Five mistakes to stop making today

  1. Undersized results. Addition needs one extra bit, multiplication needs the sum of operand widths. Decide the destination width from the math, not from the inputs.
  2. Mixing & and &&. Datapath gets bitwise, conditions get logical. They agree only for 1-bit operands.
  3. Testing for X with ==. It returns X, never 1. Use === in testbenches; design RTL so X never needs testing.
  4. >> on signed data. Use >>> with a signed operand, or your negative numbers become large positives.
  5. Trusting precedence. If an expression mixes families, add parentheses. The synthesizer does not need them; the engineer reviewing your code at 11 pm does.

Quick check

An 8-bit reg holds 8'd200 + 8'd100. What value does it contain, and why?

44. The true sum is 300, which needs 9 bits; an 8-bit destination keeps only the lower 8 bits, so the result wraps modulo 256: 300 − 256 = 44. Widen the destination to 9 bits to keep the carry.

For p = 4'b1100 and q = 4'b1010, what do p & q, p && q and &p each return?

p & q = 4’b1000 (lane-by-lane AND), p && q = 1 (both operands are non-zero, so logically true), &p = 0 (reduction AND is 1 only if every bit of p is 1, and p has two zeros).

Why does if (sig == 1'bx) never execute its true branch, and what should you write instead?

Any == comparison involving X evaluates to X, not 1, and an X condition takes the else-path. Use case equality — if (sig === 1'bx) — which compares bit patterns literally and returns a clean 0 or 1. Keep === in testbenches only; it is not synthesizable.

What does the replication expression {3{2'b01}} produce?

6’b010101 — the 2-bit pattern 01 repeated three times and concatenated, MSB first. The same idiom sign-extends a byte: {{8{d[7]}}, d} copies the sign bit eight times above the original value.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD