Skip to content

SystemVerilog · Module 4

Logical Operators

&&, ||, ! — short-circuit evaluation, X propagation, common confusion with bitwise.

Module 4 · Page 4.3

The One-Character Bug That Changes Everything

Consider this condition in a driver: if (valid & ready). It looks correct, compiles without error, and passes code review because & and && look nearly identical. But they are fundamentally different operators. & is bitwise AND — it operates on every bit of both operands and returns a multi-bit result. && is logical AND — it reduces both operands to a boolean (0 or 1) and returns a single-bit result.

When valid and ready are single-bit signals, & and && produce the same result. But when they are multi-bit buses — status registers, opcodes, packet fields — the behavior diverges and the bug becomes very hard to see. A status register 8'h02 ANDed bitwise with 8'h01 gives 8'h00, which is false. The same two values through && give 1 (both are nonzero), which is true. Completely opposite outcomes.

What Logical Operators Actually Do

Logical operators answer a boolean question: is this condition true or false? They do this in two steps. First, each operand is reduced to a single bit — any nonzero value becomes 1, zero stays 0, and X/Z become X. Second, the boolean operation is applied to those reduced single bits.

This reduction step is the key distinction from bitwise operators. 8'hF0 && 8'h0F reduces to 1 && 1 = 1, because both values are nonzero. 8'hF0 & 8'h0F gives 8'h00 because the bits don't overlap — a completely different answer.

Logical AND — &&

Both operands must be nonzero for result to be 1. Reduces each operand to boolean first. Use in if conditions, loop guards, assertions.

|| — Logical OR

At least one operand must be nonzero for result to be 1. Short-circuits: if first operand is nonzero, second is not evaluated.

! — Logical NOT

Reduces operand to boolean, then inverts. !8'hFF = 0 (nonzero → 1 → inverted to 0). !8'h00 = 1. Single-bit output.

Logical vs Bitwise — The Side-by-Side Comparison

OperationLogicalBitwiseKey difference
AND&&&Logical: reduces to boolean first. Bitwise: operates on every bit pair
OR|||Same distinction — logical collapses to 0/1, bitwise preserves width
NOT!~Logical: single-bit result. Bitwise: inverts every bit, result same width as input
Result widthAlways 1 bitSame width as operandsFundamental output difference
Short-circuitYes — second operand may not evaluateNo — both operands always evaluatedHas side-effect implications in complex expressions

Syntax & Truth Tables

SystemVerilog — Logical Operator Syntax
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Logical AND — both must be true (nonzero) ────────────────────
if (valid && ready)       drive_data();
if (count > 0 && !error) process();
 
// ── Logical OR — at least one must be true ────────────────────────
if (timeout || abort)     stop_sim();
if (a == 0 || b == 0)   skip_multiply();
 
// ── Logical NOT — boolean inversion ──────────────────────────────
if (!done)                wait_more();
if (!$isunknown(sig))     sample_coverage();
 
// ── !  on multi-bit values ───────────────────────────────────────
bit r;
r = !8'hFF;    // r = 0  (FF is nonzero → true → !true = 0)
r = !8'h00;    // r = 1  (00 is zero  → false → !false = 1)
r = !8'hA5;    // r = 0  (A5 is nonzero → 0)
 
// ── Return type — always 1-bit ────────────────────────────────────
bit [7:0] a = 8'hF0, b = 8'h0F;
bit        logical_result = a && b;  // 1  — both nonzero
bit [7:0] bitwise_result = a &  b;  // 8'h00 — no overlapping bits

Truth Table — Logical AND (&&)

a (reduced)b (reduced)a && b
000
010
100
111
0X0 ← short-circuit: left is 0, right is not evaluated
1XX ← left does not decide; right is evaluated, and it is X
X00not short-circuit: right is evaluated, and 0 forces 0
X1X ← right is evaluated; 1 does not force a result
XXX

Truth Table — Logical OR (||)

a (reduced)b (reduced)a || b
000
011
101
111
1X1 ← short-circuit: left is nonzero, right is not evaluated
0XX ← left does not decide; right is evaluated, and it is X
X11not short-circuit: right is evaluated, and 1 forces 1
X0X ← right is evaluated; 0 does not force a result
XXX

Truth Table — Logical NOT (!)

a (any value)Reduces to!a
0 / 8'h00 / 32'h00 (false)1
1 / any nonzero1 (true)0
X or Z (any bit)XX

Step-by-Step Visual Evaluation

Logical vs Bitwise on Multi-Bit Values

This table is the clearest way to see where the two operator families diverge. Same input values, completely different results:

aba && b (logical)a & b (bitwise)In if(), logical givesIn if(), bitwise gives
8'hF08'h0F1 (both nonzero)8'h00 (no common bits)TRUE — branch takenFALSE — branch skipped!
8'hFF8'hFF18'hFFTRUETRUE (nonzero)
8'h018'h021 (both nonzero)8'h00 (bits don't overlap)TRUEFALSE — wrong!
8'h008'hFF0 (first is zero)8'h00FALSEFALSE (agree here)

! vs ~ on Multi-Bit Values

Value!val (logical NOT)~val (bitwise NOT)Type of result
8'hFF1'b0 — nonzero reduces to 1, inverted8'h00 — every bit flippedLogical: 1-bit. Bitwise: 8-bit
8'h001'b1 — zero reduces to 0, inverted8'hFF — every bit flippedLogical: 1-bit. Bitwise: 8-bit
8'hA51'b0 — nonzero, so 08'h5A — bits invertedCompletely different values
8'hxx1'bx8'hxxBoth propagate X

Short-Circuit Evaluation — Simulator Behavior Timeline

ExpressionLeft operandRight operand evaluated?Result
0 && func()0 (false)No — short-circuit stops here0
1 && func()1 (true)Yes — must check rightfunc() result
1 || func()1 (true)No — short-circuit stops here1
0 || func()0 (false)Yes — must check rightfunc() result
0 & func()0Yes — bitwise, no short-circuit0 (but func ran)
1 | func()all 1sYes — bitwise, no short-circuitall 1s (but func ran)

Code Examples — From Basics to Production

Example 1 — Beginner: All Three Logical Operators

Example 1 — Logical Operator Basics
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_logical_basic;
 
  int a = 10, b = 0, c = 5;
 
  initial begin
 
    // ── && — both must be nonzero ─────────────────────────────────
    $display("a && c  : %0b", a && c);    // 1  (10 && 5  — both nonzero)
    $display("a && b  : %0b", a && b);    // 0  (10 && 0  — b is zero)
    $display("b && b  : %0b", b && b);    // 0
 
    // ── || — at least one must be nonzero ────────────────────────
    $display("a || b  : %0b", a || b);    // 1  (10 || 0  — a is nonzero)
    $display("b || b  : %0b", b || b);    // 0  (0  || 0)
    $display("a || c  : %0b", a || c);    // 1
 
    // ── ! — boolean inversion ────────────────────────────────────
    $display("!a      : %0b", !a);         // 0  (a=10, nonzero → 1 → !1 = 0)
    $display("!b      : %0b", !b);         // 1  (b=0,  zero   → 0 → !0 = 1)
    $display("!!a     : %0b", !!a);        // 1  (double negate — boolean normalise)
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a && c  : 1
a && b  : 0
b && b  : 0
a || b  : 1
b || b  : 0
a || c  : 1
!a      : 0
!b      : 1
!!a     : 1

Example 2 — Intermediate: Logical vs Bitwise on Multi-Bit Values

This is the core comparison. The same values produce different results depending on which operator family you use.

Example 2 — Logical vs Bitwise Divergence
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_logical_vs_bitwise;
 
  logic [7:0] status_a = 8'hF0;   // nonzero — "has activity"
  logic [7:0] status_b = 8'h0F;   // nonzero — "has activity"
  logic [7:0] bitwise_result;
  bit          logical_result;
 
  initial begin
    logical_result = status_a && status_b;
    bitwise_result = status_a &  status_b;
 
    $display("status_a    = 0x%0h (%08b)", status_a, status_a);
    $display("status_b    = 0x%0h (%08b)", status_b, status_b);
    $display("a && b     = %0b   (logical AND)",  logical_result);
    $display("a &  b     = 0x%0h (bitwise AND)", bitwise_result);
 
    // In if conditions — completely different control flow
    if (status_a && status_b)
      $display("LOGICAL:  both statuses active — branch TAKEN");
 
    if (status_a & status_b)    // 8'h00 — evaluates as FALSE
      $display("BITWISE:  this will NOT print");
    else
      $display("BITWISE:  no bit overlap — branch SKIPPED (wrong!)");
 
    // ! vs ~ on same value
    $display("!status_a  = %0b   (logical: nonzero → 0)",  !status_a);
    $display("~status_a  = 0x%0h (bitwise: all bits flipped)", ~status_a);
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
status_a    = 0xf0 (11110000)
status_b    = 0x0f (00001111)
a && b     = 1   (logical AND)
a &  b     = 0x0 (bitwise AND)
LOGICAL:  both statuses activebranch TAKEN
BITWISE:  no bit overlapbranch SKIPPED (wrong!)
!status_a  = 0   (logical: nonzero0)
~status_a  = 0xf (bitwise: all bits flipped)

Example 3 — Verification-Oriented: Guard Conditions in Drivers

Example 3 — Logical Operators in Verification Guards
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class AhbDriver;
 
  bit enabled    = 1;
  int err_count  = 0;
  int pkt_budget = 100;
 
  // ── Multi-condition guard — all && must be true ───────────────
  function bit can_drive();
    return (enabled && !err_count && pkt_budget > 0);
    // enabled=1 AND no errors AND budget remaining
    // If ANY is false → short-circuits → returns 0
  endfunction
 
  // ── OR for alternative conditions ────────────────────────────
  function bit should_abort(bit timeout, bit fatal_err);
    return (timeout || fatal_err);
    // Either condition alone is sufficient to abort
  endfunction
 
  // ── ! for clean enable inversion ─────────────────────────────
  task drive_packet(input [31:0] data);
    if (!can_drive()) begin
      $warning("Driver blocked — not driving");
      return;
    end
    pkt_budget--;
    $display("[DRV] Driving 0x%08h", data);
  endtask
 
endclass
 
module tb_driver;
  AhbDriver drv;
 
  initial begin
    drv = new();
    drv.drive_packet(32'hA5A5_A5A5);   // OK — all conditions met
    drv.err_count = 1;
    drv.drive_packet(32'h1234_5678);   // Blocked — err_count nonzero
    $finish;
  end
endmodule

Example 4 — Corner Case: Short-Circuit with Function Side Effects

Example 4 — Short-Circuit Side Effect Corner Case
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_short_circuit;
 
  int call_count = 0;
 
  function int side_effect_fn();
    call_count++;
    $display("[FN] called — count now %0d", call_count);
    return 1;
  endfunction
 
  initial begin
 
    // && short-circuit: left=0, right function never called
    call_count = 0;
    if (0 && side_effect_fn())
      $display("branch taken");
    $display("After 0 && fn: call_count=%0d", call_count);   // 0 — fn not called
 
    // || short-circuit: left=1, right function never called
    call_count = 0;
    if (1 || side_effect_fn())
      $display("branch taken");
    $display("After 1 || fn: call_count=%0d", call_count);   // 0 — fn not called
 
    // Bitwise &: NO short-circuit — both sides always evaluate
    call_count = 0;
    if (0 & side_effect_fn())
      $display("branch taken");
    $display("After 0 &  fn: call_count=%0d", call_count);   // 1 — fn WAS called!
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
After 0 && fn: call_count=0
branch taken
After 1 || fn: call_count=0
[FN] calledcount now 1
After 0 &  fn: call_count=1

Waveform & Simulation Thinking

X Propagation in Logical Operators

Logical operators have a nuanced relationship with X. Because they reduce their operands to a boolean first, some X inputs can be "absorbed" by a definitive answer on the other side. Specifically:

  • 0 && X = 0 — regardless of X, AND with 0 is always 0
  • 1 || X = 1 — regardless of X, OR with 1 is always 1
  • 1 && X = X — result depends on the unknown
  • 0 || X = X — result depends on the unknown
  • !X = X — cannot determine boolean of unknown

This X-absorption behavior is specific to logical operators. Bitwise operators always propagate X — 0 & X gives X (not 0), because bitwise AND doesn't know whether the X bit should be 0 or 1 before applying the mask.

Synthesis Behavior

OperatorSynthesizableIn RTL
&&, ||, !✅ YesSynthesis converts operands to single bit (OR-reduction) then applies gate. Equivalent to: |a & |b for a && b
Short-circuit behaviorN/A in hardwareHardware always evaluates all inputs. Short-circuit is a simulation optimization only — synthesis sees the full boolean expression

Where You'll Use These in Real Projects

Real Verification Usage Patterns
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 1. SVA disable iff — suppress assertion during reset/X ────────
assert property (
  @(posedge clk) disable iff (!rst_n || $isunknown(data))
  valid |-> ##1 ack
) else $error("ACK not received");
// !rst_n: suppress during reset
// $isunknown(data): suppress when data has X — avoids false failures
 
// ── 2. Queue access guard — short-circuit prevents out-of-bounds ──
if (exp_q.size() > 0 && exp_q[0].id == dut_id) begin
  // exp_q[0] only accessed if queue is nonempty — safe
  exp_q.pop_front();
end
 
// ── 3. Multi-condition constraint guard ───────────────────────────
constraint c_valid_op {
  // Only set error flag if in error injection mode AND budget remains
  err_inject && (err_budget > 0) -> err_flag == 1;
}
 
// ── 4. Coverage sampling guard ────────────────────────────────────
function void sample_cov(logic [7:0] op, logic valid);
  if (valid && !$isunknown(op))
    cov_group.sample();
endfunction
 
// ── 5. Timeout abort condition ────────────────────────────────────
always @(posedge clk) begin
  if (watchdog_expired || fatal_protocol_error) begin
    $error("[TB] Simulation aborted");
    $finish;
  end
end

Common Bugs & How to Debug Them

Bug 1 — & Instead of && in if Condition

Bug 1 — Buggy: Bitwise & in Boolean Guard
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [7:0] pkt_type   = 8'h02;   // bit 1 set — "type B"
logic [7:0] wr_enable  = 8'h01;   // bit 0 set — "write enabled"
 
// BUGGY: programmer checks "both signals are nonzero"
// But bitwise & of 0x02 and 0x01 = 0x00 — evaluates as FALSE
if (pkt_type & wr_enable)
  $display("Write packet — sending");   // NEVER fires despite both being nonzero
else
  $display("Not a write packet");      // Always fires — wrong
Bug 1 — Fixed: Use && for Boolean Check
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// FIX: use && to check "are both signals nonzero?"
if (pkt_type && wr_enable)
  $display("Write packet — sending");   // Fires correctly
 
// If the intent was "does pkt_type have the write bit set?"
// use a specific bit mask with bitwise &:
localparam bit [7:0] WRITE_BIT = 8'h01;
if (pkt_type & WRITE_BIT)               // bitwise mask check — intentional
  $display("Write bit set in pkt_type");

Bug 2 — ! on Multi-Bit: Expected Bitwise Invert, Got Boolean

Bug 2 — Buggy: ! Used Where ~ Was Needed
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [7:0] mask = 8'h0F;
logic [7:0] inv_mask;
 
// BUGGY: programmer wants to invert the mask bits
inv_mask = !mask;   // !8'h0F → 8'h0F is nonzero → 1 → inv_mask = 8'h00
                    // WRONG: expected 8'hF0, got 8'h00
$display("inv_mask = 0x%0h", inv_mask);   // prints: 0x0
Bug 2 — Fixed: Use ~ for Bitwise Inversion
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [7:0] mask = 8'h0F;
logic [7:0] inv_mask;
 
// CORRECT: ~ inverts every bit in the vector
inv_mask = ~mask;   // ~8'h0F → 8'hF0
$display("inv_mask = 0x%0h", inv_mask);   // prints: 0xf0

Bug 3 — Short-Circuit Skips a Side Effect the Author Relied On

Bug 3 — Short-Circuit Skips Needed Function Call
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int pkt_count = 0;
 
function int get_and_count();
  pkt_count++;
  return 1;
endfunction
 
// BUGGY: programmer expects get_and_count() to always run
// But if left side is 0, short-circuit prevents the call
bit gate = 0;
if (gate && get_and_count())
  $display("branch taken");
 
$display("pkt_count = %0d", pkt_count);   // 0 — function was never called!
// Programmer expected 1
Bug 3 — Fixed: Separate Side-Effect from Guard
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// FIX: call the function first, capture result, then use in condition
bit  gate   = 0;
bit  fn_res = get_and_count();   // always called, side effect guaranteed
if (gate && fn_res)
  $display("branch taken");
 
$display("pkt_count = %0d", pkt_count);   // 1 — correct

Proving It — Reduction, X, and What Actually Gets Evaluated

The two facts this chapter turns on — that && reduces its operands, and that short-circuit is about evaluation rather than value — are both directly observable. This testbench observes them.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// logical_semantics_proof.sv
//
// Self-checking proof of: logical vs bitwise on multi-bit operands, the X
// truth-table rows, and the difference between "the result is decided" and
// "the right operand was skipped".
module logical_semantics_proof;
 
  int errors = 0;
  int call_count;          // counts evaluations of the right operand
 
  function automatic bit probe(bit ret);
    call_count++;          // the SIDE EFFECT we are watching for
    return ret;
  endfunction
 
  task automatic chk(string name, logic [31:0] got, logic [31:0] exp);
    if (got !== exp) begin
      errors++;
      $display("FAIL %-46s got=%0h exp=%0h", name, got, exp);
    end else
      $display("pass %-46s = %0h", name, got);
  endtask
 
  task automatic chk_calls(string name, int got, int exp);
    if (got != exp) begin
      errors++;
      $display("FAIL %-46s calls=%0d exp=%0d", name, got, exp);
    end else
      $display("pass %-46s calls=%0d", name, got);
  endtask
 
  initial begin
    logic [7:0] a, b;
    logic       x;
 
    // ================================================================
    // 1. LOGICAL REDUCES, BITWISE PRESERVES
    // ================================================================
    a = 8'hF0; b = 8'h0F;
    chk("8'hF0 && 8'h0F  (both nonzero -> true)", a && b, 1'b1);
    chk("8'hF0 &  8'h0F  (no common bits)",       a &  b, 8'h00);
    // In a condition these take OPPOSITE branches. Same operands.
 
    a = 8'hA5;
    chk("!8'hA5  (reduce, then invert)", !a, 1'b0);
    chk("~8'hA5  (invert each bit)",     ~a, 8'h5A);
 
    // && on multi-bit operands is really (|a) & (|b):
    a = 8'h80; b = 8'h01;
    chk("a && b  ==  (|a) & (|b)", a && b, (|a) & (|b));
 
    // ================================================================
    // 2. THE X ROWS - values, computed from BOTH operands
    // ================================================================
    x = 1'bx;
    chk("x && 1'b0  ->  0  (0 forces the result)",  x && 1'b0, 1'b0);
    chk("x && 1'b1  ->  x  (nothing forces it)",    x && 1'b1, 1'bx);
    chk("x || 1'b1  ->  1  (1 forces the result)",  x || 1'b1, 1'b1);
    chk("x || 1'b0  ->  x  (nothing forces it)",    x || 1'b0, 1'bx);
    chk("!x         ->  x",                         !x,        1'bx);
 
    // ================================================================
    // 3. SHORT-CIRCUIT IS ABOUT EVALUATION, NOT VALUE
    //    Same value produced; different number of calls.
    // ================================================================
    call_count = 0;
    void'(1'b0 && probe(1'b1));
    chk_calls("0 && probe()   - left decides, SKIPPED", call_count, 0);
 
    call_count = 0;
    void'(1'b1 && probe(1'b1));
    chk_calls("1 && probe()   - left decides nothing",  call_count, 1);
 
    call_count = 0;
    void'(x && probe(1'b0));
    chk_calls("x && probe()   - X decides NOTHING",     call_count, 1);
    // ^ This is the check that refutes "X short-circuits like 0".
    //   The value is 0, exactly as `0 && probe()` gives - but the
    //   function RAN. Value and evaluation are different questions.
 
    call_count = 0;
    void'(1'b0 & probe(1'b1));
    chk_calls("0 &  probe()   - bitwise never skips",   call_count, 1);
 
    call_count = 0;
    void'(1'b1 || probe(1'b1));
    chk_calls("1 || probe()   - left decides, SKIPPED", call_count, 0);
 
    if (errors == 0) $display("\nlogical_semantics_proof: ALL CHECKS PASSED");
    else             $display("\nlogical_semantics_proof: %0d FAILURES", errors);
    $finish;
  end
endmodule

Section 3 is the one that settles the argument. 0 && probe() and x && probe() both produce 0. One calls the function and one does not. If short-circuit were about the value being determined, they would behave the same; they do not.

Two multi-bit operands feed both a logical and a bitwise AND. The logical path OR-reduces each operand to one bit then ANDs, giving true. The bitwise path ANDs per bit position, giving zero. Used as a condition the two take opposite branches.a = 8'hF01111_0000b = 8'h0F0000_1111OR-reduce each: a &&b(|a) & (|b) - 1 bitPer-bit AND: a & b8 bits, width keptResult 1'b1 — TRUEbranch is takenResult 8'h00 — FALSEbranch is skipped12
Figure 1 - the same two operands down two paths. A logical operator OR-reduces each operand to a single truth value and then applies AND, so an eight-bit input becomes one bit before the operation happens. A bitwise operator applies AND at every bit position and keeps the width. The results diverge whenever the operands have no bit position in common: the logical form is true because both are nonzero, the bitwise form is zero because nothing overlaps, and used as a condition they take opposite branches.
1

A per-lane arbiter mask collapsed to one bit when & was changed to &&

LOGICAL-COLLAPSED-A-VECTOR
Buggy Code & Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: a lint rule flagged "& used in a boolean context" and the fix
//         applied was to change it to &&. The expression was never a
//         boolean context - it builds a per-lane mask.
module lane_arbiter #(parameter int LANES = 8) (
  input  logic [LANES-1:0] lane_valid,
  input  logic [LANES-1:0] lane_ready,
  output logic [LANES-1:0] lane_go
);
  // The lint warning was about a DIFFERENT line. This one was "fixed" too.
  assign lane_go = lane_valid && lane_ready;   // ❌ collapses to 1 bit
endmodule
 
// What this computes for lane_valid = 8'b0000_1010, lane_ready = 8'b0000_0010:
//   (|8'b0000_1010) & (|8'b0000_0010)  =  1 & 1  =  1'b1
//   assigned to an 8-bit output -> zero-extended -> 8'b0000_0001
//
//   Lane 1 SHOULD go (valid and ready).  Lane 0 goes instead.
//   Lane 3 is valid but not ready, and is correctly held off - by accident.
 
// ✅ FIX: this is a value, not a question. Per-bit AND preserves the lanes.
assign lane_go = lane_valid & lane_ready;      // ✅ 8'b0000_0010
Symptom

An 8-lane arbiter passed its block-level regression and every single-lane system test. It failed intermittently in a multi-lane traffic scenario, in a way that took three weeks to characterise:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
lane_valid = 8'b0000_1010    (lanes 1 and 3 have data)
lane_ready = 8'b0000_0010    (only lane 1 can accept)
 
expected lane_go = 8'b0000_0010   (lane 1)
observed lane_go = 8'b0000_0001   (lane 0 - which has no data at all)

Lane 0 was being granted while idle, and the lane that actually had a ready sink was starved. On single-lane traffic — where all activity is on lane 0 — the behaviour was indistinguishable from correct.

The change that introduced it was a one-character edit made while clearing lint warnings, in a commit whose message was "fix lint."

Root Cause

&& reduces. lane_valid && lane_ready is (|lane_valid) & (|lane_ready) — a single bit meaning "some lane has data and some lane is ready", which is a true statement about the system and completely useless as a grant vector. Assigned to an 8-bit output it zero-extends, so the one bit lands on lane 0.

The result is a grant signal that is right about the aggregate and wrong about every individual lane. That is a much more insidious failure than a dead output, because the block continues to do something plausible: traffic flows, lanes make progress, and the arbiter's own status counters look reasonable.

Three things kept it hidden.

Single-lane tests cannot see it. When all activity is on lane 0, the collapsed bit lands where the correct bit would have been. Every directed test in the suite drove one lane at a time, because that is how the lanes were brought up.

The lint warning was real, but about a different line. Elsewhere in the module & genuinely was used in a condition and genuinely should have been &&. The engineer applied the same change to both, which is the pattern to watch for when clearing a class of warnings: the rule identifies a shape, not an intent.

No tool can flag it. Both expressions are legal, both type-check, and both produce a value assignable to an 8-bit output. There is no width mismatch to warn about, because a 1-bit value zero-extends silently.

The general principle: && asks a question, & computes a value. The test is what the result feeds. An if, a while, an assertion antecedent — those want a question, and && is correct. A data path, a mask register, a per-bit enable — those want a value, and reducing it destroys the per-lane information by construction, after which no downstream logic can recover it.

Fix

Restore the bitwise operator, then make the collapse impossible to reintroduce silently.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 1. The fix.
assign lane_go = lane_valid & lane_ready;
 
// 2. A width assertion at the point of definition. A logical operator
//    cannot satisfy this, because its result is one bit and the elaboration
//    -time check on the expression width fails.
generate
  if ($bits(lane_valid & lane_ready) != LANES)
    $error("lane_go expression is not LANES bits wide - a logical operator "
         , "has collapsed it");
endgenerate
 
// 3. The property that catches it in simulation regardless of how it was
//    written: a lane may only be granted if that lane is valid and ready.
a_grant_is_per_lane: assert property (@(posedge clk) disable iff (!rst_n)
  (lane_go & ~(lane_valid & lane_ready)) == '0)
  else $error("granted a lane that was not both valid and ready");
 
// 4. The coverage that proves the suite could have found it. A bin for
//    "two or more lanes valid simultaneously" sitting at zero hits means
//    every test drove one lane at a time.
covergroup cg_lanes;
  cp_concurrency : coverpoint $countones(lane_valid) {
    bins single   = {1};
    bins multiple = {[2:LANES]};   // zero hits => this bug was unreachable
  }
endgroup

Three habits, and the third is the one that would actually have caught this:

  • Choose the operator by what the result feeds. Condition means &&; data path means &. Width is the tell — if either operand of && is wider than one bit, stop and ask.
  • Never apply a lint fix by pattern across a file. A rule matches a shape; only a reader knows the intent. A commit that changes several instances of one operator deserves an instance-by-instance review, not a single approval.
  • Cover concurrency on anything per-lane, per-channel, or per-bank. A multiple bin at zero hits after a full regression says the suite has only ever exercised the degenerate case — and in a design whose whole purpose is arbitration, the degenerate case is the one where the arbiter does not matter.

Interview Questions

Best Practices & Coding Guidelines

Use && and || in if conditions

Always use logical operators for boolean conditions in if, while, and assert. Use bitwise operators only for mask operations and bit manipulation.

! vs ~ — know which you need

Use ! to test "is this zero or nonzero." Use ~ to invert every bit in a vector. Confusing them on multi-bit signals produces wrong results silently.

Short-circuit as a safety guard

Use q.size() > 0 && q[0].field to safely access queue elements. The size check short-circuits before the index access runs.

No side effects in && or || operands

If a function has side effects (counter, display, state change), never rely on it being evaluated inside a short-circuit && or ||. Call it separately first.

PatternRecommendation
if (a && b)✅ Correct for boolean conditions on any-width signals
if (a & b)⚠️ Only correct when you specifically want bit-overlap check
if (!flag)✅ Correct boolean inversion — flag must be 1-bit or intended as boolean
inv = !mask (8-bit mask)❌ Wrong — use ~mask to invert bits
q.size() > 0 && q[0].valid✅ Safe short-circuit guard — use this pattern
0 && side_effect_fn()❌ fn never called — never rely on this in production

Summary

Three operators. Three rules to internalize and keep for life.

  • Use && and || in boolean conditions, always. They reduce operands to a single bit first. On multi-bit signals, & and | operate bit-by-bit and will produce different results whenever the bit patterns don't overlap — a common situation with status registers and opcode fields.
  • Use ~ to invert bits, ! to invert boolean. ! on a multi-bit nonzero value gives 0, not the bit-inverted value. Getting this wrong on a mask variable produces silent corruption.
  • Short-circuit evaluation means the right operand of && and || may not run. Never place a side-effecting function call inside one of these conditions if that call must always execute. Separate the call from the condition.

The operator family this page is defined against. Bitwise Operators covers &, |, ^, ~ — the per-bit forms that preserve width where these reduce it. The two pages describe the same symbols doing different jobs, and every bug on this page comes from substituting one for the other. Reduction Operators covers the unary &a, |a, ^a, which is what && is doing internally: a && b is (|a) & (|b), so a logical operator is a reduction followed by a one-bit operation.

Where these appear in practice. Conditions: see if / else, unique & priority for what a decision built on a logical expression means to synthesis, and Concurrent Assertions for antecedents, where a collapsed multi-bit expression makes a property vacuous instead of failing. For where these sit relative to the other operators — binary | binds tighter than &&, which surprises most engineers — see Operator Precedence. For the X values that make the truth-table rows on this page matter, see 2-state vs 4-state Types.

References.

  • IEEE 1800 (SystemVerilog) — the logical operators and their truth tables are in the operators-and-expressions clause, which also specifies that && and || may skip evaluating their right operand when the left operand determines the result. That permission is the whole of short-circuit evaluation, and this page was corrected against it: the truth-table rows where an X operand still yields a definite value are consequences of the AND and OR functions, not instances of short-circuit. The right operand is evaluated in those cases, which is observable through a side effect and is proven in the testbench above.
  • IEEE 1364 (Verilog) — the same operators and truth tables, inherited unchanged.

Requirement versus practice. The truth tables, the reduction-to-a-truth-value behaviour, and the short-circuit permission are language requirements. The convention of using &&/|| exclusively in conditions and &/| exclusively when building values is style — both compile either way. It is worth following because it is the only habit that survives a later widening of a signal, which is the change that turns a harmless equivalence into the arbiter bug in Debug Lab 1.

Part of SystemVerilog Fundamentals·Operators & Expressions·Lesson 21 of 53

View program

Continue learning

Standards & specifications

Governing standard
IEEE Std 1800 (SystemVerilog)(opens IEEE in a new tab)

Defines SystemVerilog language semantics — syntax, data types, scheduling and the behaviour a conforming simulator must produce. It does not define tool-specific synthesis support or vendor methodology.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of SystemVerilog Operators — Width, Signedness & X Behaviour.