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
| Operation | Logical | Bitwise | Key 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 width | Always 1 bit | Same width as operands | Fundamental output difference |
| Short-circuit | Yes — second operand may not evaluate | No — both operands always evaluated | Has side-effect implications in complex expressions |
Syntax & Truth Tables
// ── 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 bitsTruth Table — Logical AND (&&)
| a (reduced) | b (reduced) | a && b |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
| 0 | X | 0 ← short-circuit: left is 0, right is not evaluated |
| 1 | X | X ← left does not decide; right is evaluated, and it is X |
| X | 0 | 0 ← not short-circuit: right is evaluated, and 0 forces 0 |
| X | 1 | X ← right is evaluated; 1 does not force a result |
| X | X | X |
Truth Table — Logical OR (||)
| a (reduced) | b (reduced) | a || b |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
| 1 | X | 1 ← short-circuit: left is nonzero, right is not evaluated |
| 0 | X | X ← left does not decide; right is evaluated, and it is X |
| X | 1 | 1 ← not short-circuit: right is evaluated, and 1 forces 1 |
| X | 0 | X ← right is evaluated; 0 does not force a result |
| X | X | X |
Truth Table — Logical NOT (!)
| a (any value) | Reduces to | !a |
|---|---|---|
| 0 / 8'h00 / 32'h0 | 0 (false) | 1 |
| 1 / any nonzero | 1 (true) | 0 |
| X or Z (any bit) | X | X |
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:
| a | b | a && b (logical) | a & b (bitwise) | In if(), logical gives | In if(), bitwise gives |
|---|---|---|---|---|---|
8'hF0 | 8'h0F | 1 (both nonzero) | 8'h00 (no common bits) | TRUE — branch taken | FALSE — branch skipped! |
8'hFF | 8'hFF | 1 | 8'hFF | TRUE | TRUE (nonzero) |
8'h01 | 8'h02 | 1 (both nonzero) | 8'h00 (bits don't overlap) | TRUE | FALSE — wrong! |
8'h00 | 8'hFF | 0 (first is zero) | 8'h00 | FALSE | FALSE (agree here) |
! vs ~ on Multi-Bit Values
| Value | !val (logical NOT) | ~val (bitwise NOT) | Type of result |
|---|---|---|---|
8'hFF | 1'b0 — nonzero reduces to 1, inverted | 8'h00 — every bit flipped | Logical: 1-bit. Bitwise: 8-bit |
8'h00 | 1'b1 — zero reduces to 0, inverted | 8'hFF — every bit flipped | Logical: 1-bit. Bitwise: 8-bit |
8'hA5 | 1'b0 — nonzero, so 0 | 8'h5A — bits inverted | Completely different values |
8'hxx | 1'bx | 8'hxx | Both propagate X |
Short-Circuit Evaluation — Simulator Behavior Timeline
| Expression | Left operand | Right operand evaluated? | Result |
|---|---|---|---|
0 && func() | 0 (false) | No — short-circuit stops here | 0 |
1 && func() | 1 (true) | Yes — must check right | func() result |
1 || func() | 1 (true) | No — short-circuit stops here | 1 |
0 || func() | 0 (false) | Yes — must check right | func() result |
0 & func() | 0 | Yes — bitwise, no short-circuit | 0 (but func ran) |
1 | func() | all 1s | Yes — bitwise, no short-circuit | all 1s (but func ran) |
Code Examples — From Basics to Production
Example 1 — Beginner: All Three Logical Operators
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
endmoduleExpected output:
a && c : 1
a && b : 0
b && b : 0
a || b : 1
b || b : 0
a || c : 1
!a : 0
!b : 1
!!a : 1Example 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.
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
endmoduleExpected output:
status_a = 0xf0 (11110000)
status_b = 0x0f (00001111)
a && b = 1 (logical AND)
a & b = 0x0 (bitwise AND)
LOGICAL: both statuses active — branch TAKEN
BITWISE: no bit overlap — branch SKIPPED (wrong!)
!status_a = 0 (logical: nonzero → 0)
~status_a = 0xf (bitwise: all bits flipped)Example 3 — Verification-Oriented: Guard Conditions in Drivers
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
endmoduleExample 4 — Corner Case: Short-Circuit with Function Side Effects
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
endmoduleExpected output:
After 0 && fn: call_count=0
branch taken
After 1 || fn: call_count=0
[FN] called — count now 1
After 0 & fn: call_count=1Waveform & 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 01 || X= 1 — regardless of X, OR with 1 is always 11 && X= X — result depends on the unknown0 || 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
| Operator | Synthesizable | In RTL |
|---|---|---|
&&, ||, ! | ✅ Yes | Synthesis converts operands to single bit (OR-reduction) then applies gate. Equivalent to: |a & |b for a && b |
| Short-circuit behavior | N/A in hardware | Hardware 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
// ── 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
endCommon Bugs & How to Debug Them
Bug 1 — & Instead of && in if Condition
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// 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
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: 0x0logic [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: 0xf0Bug 3 — Short-Circuit Skips a Side Effect the Author Relied On
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// 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 — correctProving 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.
// 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
endmoduleSection 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.
A per-lane arbiter mask collapsed to one bit when & was changed to &&
LOGICAL-COLLAPSED-A-VECTOR// ❌ 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_0010An 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:
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."
&& 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.
Restore the bitwise operator, then make the collapse impossible to reintroduce silently.
// 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
}
endgroupThree 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
multiplebin 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.
| Pattern | Recommendation |
|---|---|
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.
Related Pages & References
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 programContinue learning
Related tutorials
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.