Chapter 7 of 16
Operators and Procedural Statements
A quick pass over the parts of if/case/loops you already know from general programming, spending the real time on what's actually new in SystemVerilog: repeat/forever loops, unique/priority case, bitwise vs. logical operators, 4-state case equality ===/!==, increment/decrement operators, and streaming operators.
If you've written any mainstream programming language, if/else, case (switch), and loops aren't new to you — this chapter won't re-teach their basics from scratch. Instead it moves quickly through the parts that match what you already know, and spends the real space on the handful of SystemVerilog-specific things actually worth learning.
The familiar part: if/else and loops
if/else if/else work the way they do in most languages; for/while/do...while carry the same semantics too. SystemVerilog adds two extra loop constructs that show up constantly in hardware description and verification code:
// repeat: run a fixed number of times
initial begin
repeat (3) begin
$display("hello");
end
end
// forever: an infinite loop, most commonly used to generate a clock
initial begin
clk = 0;
forever #5 clk = ~clk; // toggles every 5 time units, producing a period-10 clock
endrepeat(N) is shorthand for "run this N times." forever is an infinite loop — it almost never shows up alone in design code, but it's extremely common in verification code for generating clocks or driving continuous stimulus. The forever #5 clk = ~clk; pattern above is a clock-generation template you'll see in nearly every testbench.
The case statement: no implicit fall-through
case behaves more like Python's match or Rust's match than C/Java's switch — each matched branch runs and the whole case ends automatically; it doesn't "fall through" to the next branch the way C's switch does when you forget a break:
case (cur_state)
IDLE: next_state = RUN;
RUN: next_state = DONE;
DONE: next_state = IDLE;
default: next_state = IDLE;
endcaseOnce the RUN branch matches, only next_state = DONE; runs, and the whole case ends right there — no break needed, because there's no fall-through to guard against.
unique case and priority case: letting the tool check branch quality
A plain case statement never checks whether your branches are exhaustive (cover every possible value) or mutually exclusive (what happens if more than one matches at once) — the most common consequence of an incomplete branch list is an unintentionally inferred latch, a bug class that's often subtle to spot. SystemVerilog provides two modifier keywords that get the simulator and synthesis tool to check this for you at compile/elaboration/simulation time:
unique case (sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
2'b11: y = d;
endcaseunique case: tells the tool "these branches should be mutually exclusive and should cover every possible value" — ifselmatches more than one branch during simulation, or matches none at all, the tool flags a warning/error instead of silently taking the first match (or doing nothing) like a plaincasewould.priority case: tells the tool "these branches have priority in the order written, and the first match wins," while still requiring at least one branch to match, warning otherwise.
Neither keyword changes the actual runtime matching behavior — it's still "whichever branch matches, matches" — but both help you catch "I forgot a branch" during development, instead of discovering an unintended latch only after synthesis.
Bitwise vs. logical operators: &/|/^/~ vs. &&/||/!
These two operator groups look similar but mean completely different things — an easy trap for newcomers:
- Bitwise operators (
&,|,^,~) operate on two operands bit by bit — if the operands are multi-bit vectors, the result is a vector of the same width, with each bit computed independently. - Logical operators (
&&,||,!) treat an entire operand as a single true/false value (nonzero is true), and the result is always a 1-bit boolean.
logic [3:0] a = 4'b1100;
logic [3:0] b = 4'b1010;
$display("a & b = %b", a & b); // bitwise AND, computed per-bit: 1000
$display("a && b = %b", a && b); // logical AND: a is nonzero and b is nonzero, result is 1a & b ANDs each bit of a and b independently, giving back a 4-bit vector; a && b first checks whether a and b are each nonzero (true), then ANDs those two booleans, always producing a single 0/1. Using the wrong one (writing & where && was meant) doesn't error — it just silently gives a completely different result, so it's worth double-checking.
4-state case equality: === and !==
Chapter 5 covered 4-state types' two special states, x (unknown) and z (high-impedance) — but plain ==/!= behave counterintuitively around x/z, a trap nearly every beginner falls into:
logic [3:0] data = 4'bx01x;
if (data == 4'bx01x)
$display("won't print"); // == with any x/z operand yields x, and if() treats x as false==/!= (logical equality) return x (neither 0 nor 1) whenever either operand contains an x/z, and if treats x as false — so the $display above never runs; the moment data contains an x, == silently stops working the way you'd expect.
To compare bit-for-bit, treating x/z as ordinary values to match exactly, use ===/!== (case equality/inequality):
if (data === 4'bx01x)
$display("prints: === compares bit-for-bit, x included"); // this one runs==/!=: the result isxwhenever either side containsx/z— never a definite true or false.===/!==: compares bit-for-bit exactly,x/zincluded; the result is always a definite0or1, neverx.
Verification code often needs to explicitly check whether a signal is x (for example, confirming a signal was actually driven) — that requires ===/!==; plain ==/!= simply can't do it:
if (some_signal === 1'bx)
$error("some_signal was never driven!");Reduction operators
Placing &, |, or ^ (or their inverted forms ~&, ~|, ~^) before a single operand reduces every bit of that vector through the operator into a single-bit result:
logic [7:0] data = 8'b1011_0110;
$display("reduction AND = %b", &data); // 1 only if all 8 bits are 1
$display("reduction OR = %b", |data); // 1 if any bit is 1
$display("reduction XOR = %b", ^data); // XORs all 8 bits together, commonly used for parityReduction XOR (^data) is especially common — it reports whether the number of 1 bits is odd or even, making it the most compact way to implement parity (the parity function in chapter 9 uses exactly this operator).
Concatenation and replication: {} and {n{...}}
Concatenation {a, b, c} joins signals end-to-end into a wider vector; replication {n{expr}} repeats expr n times and concatenates the copies:
logic [3:0] hi = 4'hA;
logic [3:0] lo = 4'hB;
logic [7:0] combined;
combined = {hi, lo}; // concatenation: 8'hAB
combined = {8{1'b0}}; // replication: 8 zeros, i.e. 8'h00
combined = {4{2'b10}}; // replication: "10" repeated 4 times, giving 8'b10101010Both can appear on either side of an assignment, and can nest (like {a, {4{1'b0}}, b}) — a common pattern for assembling or splitting apart bus data.
Increment/decrement operators: ++ and --
Verilog has no ++/-- — you had to write i = i + 1. SystemVerilog adds both operators, supporting prefix and postfix forms like most languages:
int i = 0;
i++; // equivalent to i = i + 1, i becomes 1
++i; // also increments i by 1
i--; // decrements i by 1One thing worth clarifying, since it's easy to get wrong: ++/-- are syntactically allowed inside larger expressions (arr[idx++] = x; is a common, legal example), but if the same variable gets read and written more than once within one expression (like y = i++ + i;), the evaluation order isn't defined by the language standard, and different simulators can give different results. To avoid that potential ambiguity, this track's convention is: use ++/-- only as standalone statements, never embedded in another expression — code written that way never has an evaluation-order problem to begin with:
i++; // ✓ preferred: a standalone statement, no ambiguity
y = i++; // ⚠ avoid: legal syntax, but folding the increment into an assignment isn't this track's styleStreaming operators
The streaming operators { << {...} } and { >> {...} } group a chunk of data into fixed-width slices and re-pack them — most commonly used for byte-swapping (endianness conversion):
logic [31:0] data, swapped;
initial begin
data = 32'h1122_3344;
swapped = { << 8 {data} }; // group into 8-bit slices, reverse the group order
$display("swapped = %h", swapped); // 44332211
end{ << 8 {data} } means: slice data into 8-bit groups, then reverse the order of those groups and reassemble them — the four bytes 11 22 33 44 become 44 33 22 11, exactly the common big-endian/little-endian byte swap. Streaming operators are also commonly used to convert between "one big chunk of data" and "split into individual fields or array elements," without hand-writing a bit-by-bit or byte-by-byte loop.
Summary
if/elseandfor/while/do...whilematch most programming languages; SystemVerilog addsrepeat(N)(a fixed-count loop) andforever(an infinite loop, commonly used for clock generation).- Each
casebranch ends the whole statement when it finishes — no C-style implicit fall-through, and nobreakneeded. unique case/priority caselet the tool check branch exclusivity/exhaustiveness during development, helping catch a missing branch before it becomes an unintended latch.- Bitwise operators (
&/|/^/~) operate per-bit, giving a result as wide as the operands; logical operators (&&/||/!) treat the whole operand as true/false, always giving a 1-bit result — similar-looking symbols with very different meanings. ==/!=returnxwhenever either side containsx/z(whichiftreats as false);===/!==compare bit-for-bit exactly,x/zincluded, and always give a definite0/1— the correct way to check whether a signal isx.- Reduction operators (like
^data) reduce every bit of a vector into a single-bit result; reduction XOR (^) is commonly used for parity. - Concatenation
{a, b}joins signals into a wider vector; replication{n{expr}}repeatsexprn times and concatenates the copies — both common for assembling/splitting bus data. ++/--are new in SystemVerilog; they're syntactically allowed in expressions, but evaluation order is undefined when the same variable is read/written more than once in one expression, so the convention is to use them only as standalone statements.- The streaming operators
{ << {...} }/{ >> {...} }group data by a given width and reorder it, commonly used for byte-swapping and similar conversions.
Which statement about SystemVerilog's case statement is correct?
Which keyword, placed before case, makes the tool check that branches are mutually exclusive and exhaustive, warning if zero or multiple branches match? (lowercase)
Which description best matches the streaming operator { << 8 {data} }?
After logic [3:0] a = 4'b1100; logic [3:0] b = 4'b1010;, what's the difference between a & b and a && b?
What is reduction XOR on a multi-bit vector (like ^data) most commonly used for?
data is a 4-state signal that contains an x. What happens with if (data == 4'bx01x)?