Operator Precedence
Complete precedence table, associativity rules, classic precedence traps.
Module 4 · Page 4.10
Why Precedence Bugs Are Particularly Nasty
A type error fails at compile time. A width mismatch shows up in simulation. A precedence bug produces the wrong result silently, in clean two-state simulation, with no warning from any tool. The expression evaluates — just not the way you intended. RTL passes synthesis. Simulation matches the (wrong) expected model. The hardware works differently.
The three expressions that cause the most real-world bugs:
| Expression as written | How SV parses it | What engineer intended |
|---|---|---|
a & b == c | a & (b == c) — == binds tighter than & | (a & b) == c |
a | b && c | a | (b && c) — && binds tighter than | | (a | b) && c |
sel ? a : b + c | sel ? a : (b + c) — + binds tighter than ?: | (sel ? a : b) + c |
Complete Precedence Table — Highest to Lowest
Higher precedence means the operator binds first — it grabs its operands before lower-precedence operators do. Operators on the same row have equal precedence and are resolved by associativity (left-to-right unless noted).
| Level | Operators | Type | Associativity | Notes |
|---|---|---|---|---|
| 1 (Highest) | ( ) [ ] { } {{ }} | Grouping / index / concat / replicate | — | Explicit grouping always wins |
| 2 | + - ! ~ & ~& | ~| ^ ~^ ^~ ++ -- | Unary operators (sign, logic, reduction, inc/dec) | Right-to-left | All unary ops — same level, right-associative |
| 3 | ** | Exponentiation | Right-to-left | 2**3**2 = 2**(3**2) = 512 |
| 4 | * / % | Multiply, divide, modulo | Left-to-right | Standard arithmetic priority |
| 5 | + - | Binary add, subtract | Left-to-right | Lower than multiply |
| 6 | << >> <<< >>> | Shift operators | Left-to-right | Below arithmetic — a + b << 2 = (a+b) << 2 |
| 7 | < <= > >= | Relational (less/greater) | Left-to-right | Below shift |
| 8 | == != === !== ==? !=? | Equality / case equality / wildcard equality | Left-to-right | Higher than bitwise — the C-programmer trap |
| 9 | & | Binary bitwise AND | Left-to-right | Below equality — a & b == c = a & (b==c) |
| 10 | ^ ~^ ^~ | Binary bitwise XOR / XNOR | Left-to-right | Below bitwise AND |
| 11 | | | Binary bitwise OR | Left-to-right | Below XOR — AND | XOR | OR from top to bottom |
| 12 | && | Logical AND | Left-to-right | Below bitwise OR — a | b && c = a | (b && c) |
| 13 | || | Logical OR | Left-to-right | Below logical AND |
| 14 | inside | Set membership | Left-to-right | Same level as relational operators conceptually |
| 15 | ? : | Conditional (ternary) | Right-to-left | Very low — sel ? a : b + c = sel ? a : (b+c) |
| 16 (Lowest) | = += -= *= /= %= &= |= ^= <<= >>= | Assignment operators | Right-to-left | Always last — entire RHS evaluates before assignment |
Step-by-Step Evaluation — Seeing Precedence in Action
The Classic Bitwise-vs-Equality Trap
| Expression | Step 1 (higher prec first) | Step 2 | Final result |
|---|---|---|---|
data & 8'hF0 == 8'hA0 | 8'hF0 == 8'hA0 → 1'b0 (equality first) | data & 1'b0 → 8'h00 | 8'h00 — wrong! |
(data & 8'hF0) == 8'hA0 | data & 8'hF0 → masked data | masked == 8'hA0 → 0 or 1 | Correct boolean |
Logical vs Bitwise Mixing Trap
| Expression | Parsed as | Result (a=8'hF0, b=8'h0F, c=1'b1) |
|---|---|---|
a | b && c | a | (b && c) — && first | 8'hF0 | (8'h0F && 1) = 8'hF0 | 1 = 8'hF1 |
(a | b) && c | Bitwise OR first, then logical AND | (8'hFF) && 1 = 1 (logical: non-zero AND 1) |
Conditional Trap — Lower Operand Captured
| Expression | Parsed as | If sel=1, a=5, b=3, c=10 |
|---|---|---|
sel ? a : b + c | sel ? a : (b + c) | sel=1 → result = a = 5 |
(sel ? a : b) + c | Ternary first, then add c | sel=1 → (a) + c = 5 + 10 = 15 |
Shift and Arithmetic Interaction
| Expression | Parsed as | With a=2, b=3, n=2 |
|---|---|---|
a + b << n | (a + b) << n — add before shift | (2+3) << 2 = 5 << 2 = 20 |
a + (b << n) | Shift before add | 2 + (3 << 2) = 2 + 12 = 14 |
Code Examples — Precedence Traps in Practice
Example 1 — The Bitwise Mask Check Bug
module tb_prec_bitwise;
logic [7:0] data = 8'hA5; // 1010_0101
initial begin
// BUGGY: engineer wants to check if upper nibble == 0xA
// Written: data & 8'hF0 == 8'hA0
// Parsed: data & (8'hF0 == 8'hA0) = data & 0 = 8'h00
$display("BUGGY: data & 8'hF0 == 8'hA0 = %0h",
data & 8'hF0 == 8'hA0); // 0 — wrong!
// CORRECT: parentheses around the mask operation
$display("CORRECT: (data & 8'hF0) == 8'hA0 = %0b",
(data & 8'hF0) == 8'hA0); // 1 — upper nibble is A
// Verify the difference visually
$display("8'hF0 == 8'hA0 = %0b", 8'hF0 == 8'hA0); // 0
$display("data & 0 = %0h", data & 1'b0); // 0
$display("data & 8'hF0 = %0h", data & 8'hF0); // A0
$finish;
end
endmoduleExpected output:
BUGGY: data & 8'hF0 == 8'hA0 = 0
CORRECT: (data & 8'hF0) == 8'hA0 = 1
8'hF0 == 8'hA0 = 0
data & 0 = 0
data & 8'hF0 = A0Example 2 — Conditional and Arithmetic Precedence
module tb_prec_conditional;
logic sel = 1'b1;
logic [7:0] a = 8'd5;
logic [7:0] b = 8'd3;
logic [7:0] c = 8'd10;
logic [7:0] result;
initial begin
// BUGGY intent: add c to whichever of a or b is selected
// Written: sel ? a : b + c
// Parsed: sel ? a : (b + c) — + has higher precedence than ?:
result = sel ? a : b + c;
$display("BUGGY sel?a:b+c = %0d (parsed as sel?a:(b+c))", result); // 5
// CORRECT: parentheses enforce ternary-first
result = (sel ? a : b) + c;
$display("CORRECT (sel?a:b)+c = %0d", result); // 15 (5 + 10)
// Same trap with sel=0
sel = 1'b0;
result = sel ? a : b + c; // = 3 + 10 = 13 (b+c)
$display("sel=0: sel?a:b+c = %0d", result); // 13
result = (sel ? a : b) + c; // = 3 + 10 = 13 (same — different path, same result)
$display("sel=0: (sel?a:b)+c = %0d", result); // 13
// Bug is only visible when sel=1 — classic intermittent bug!
$finish;
end
endmoduleExpected output:
BUGGY sel?a:b+c = 5 (parsed as sel?a:(b+c))
CORRECT (sel?a:b)+c = 15
sel=0: sel?a:b+c = 13
sel=0: (sel?a:b)+c = 13Example 3 — Negation and Reduction Operator Interaction
module tb_prec_unary;
logic [3:0] data = 4'hA; // 1010
initial begin
// Trap: ~&data vs ~(&data)
$display("&data = %0b", &data); // 0 — AND reduction: 1010
$display("~(&data) = %0b", ~(&data)); // 1 — negate the AND result
$display("~&data = %0b", ~&data); // 1 — NAND reduction: same as ~(&data)
// ~& is a single NAND reduction operator, not ~ followed by & reduction
// The dangerous one: !&data
$display("!&data = %0b", !&data); // 1 — logical not of AND reduction
$display("!data = %0b", !data); // 0 — logical not of 4-bit data (non-zero → 0)
// !&data parsed as !(&data) = !(0) = 1
// Very different from !(data) = !(8'hA) = 0
// Right-associativity of unary operators
$display("~~data = %04b", ~~data); // 1010 — double invert = original
$display("!!data = %0b", !!data); // 1 — !(!(non-zero)) = !(0) = 1
$finish;
end
endmoduleExample 4 — Full Verification: All Traps in One Testbench
module tb_precedence_all;
logic [7:0] a = 8'hAA; // 1010_1010
logic [7:0] b = 8'h55; // 0101_0101
logic s = 1'b1;
logic [7:0] r;
initial begin
// ── Arithmetic before shift ───────────────────────────────────
r = 8'd2 + 8'd3 << 1; // (2+3)<<1 = 5<<1 = 10
$display("2+3<<1 = %0d (expect 10)", r);
// ── Equality above bitwise AND ─────────────────────────────────
r = a & b == 8'h00; // a & (b==0) = AA & 0 = 0
$display("a&b==0 = %0h (expect 0, NOT 1)", r);
r = (a & b) == 8'h00; // (AA&55)=00, 00==00 → 1
$display("(a&b)==0 = %0b (expect 1)", r);
// ── Bitwise OR below logical AND ──────────────────────────────
r = a | b && 1'b0; // a | (b && 0) = AA | 0 = AA
$display("a|b&&0 = %0h (expect AA)", r);
r = (a | b) && 1'b0; // (FF) && 0 = 0
$display("(a|b)&&0 = %0b (expect 0)", r);
// ── Conditional: ? : is right-associative ────────────────────
// a ? b ? c : d : e = a ? (b ? c : d) : e
logic [7:0] x = 8'h01, y = 8'h02, z = 8'h03;
r = s ? s ? x : y : z; // s=1: s?(s?x:y):z = 1?(1?x:y):z = x = 0x01
$display("s?s?x:y:z = %0h (expect 01)", r);
// ── Exponentiation is right-associative ───────────────────────
r = 2 ** 3; // 2**3 = 8
$display("2**3 = %0d (expect 8)", r);
$finish;
end
endmoduleHow the Simulator Resolves Complex Expressions
Associativity — When Precedence Is Equal
When two operators have equal precedence, associativity determines the grouping. Almost all binary operators are left-associative — they group left-to-right. The exceptions are unary operators, exponentiation, the conditional ?:, and assignment operators — these are right-associative.
| Expression | Associativity | Grouped as | Note |
|---|---|---|---|
a - b - c | Left | (a - b) - c | Standard math grouping |
a && b && c | Left | (a && b) && c | Short-circuits left to right |
2 ** 3 ** 2 | Right | 2 ** (3 ** 2) = 512 | NOT (2**3)**2 = 64 |
a ? b : c ? d : e | Right | a ? b : (c ? d : e) | Nested ternary chains naturally |
a = b = c | Right | a = (b = c) | c assigned to b first, then a |
Width Determination and Precedence
Precedence determines the parse tree, but the result width at each node is determined by the operand widths and operator rules. Parentheses that change grouping can therefore also change the result width of intermediate sub-expressions. Example: 8'hFF + 1 inside a 16-bit context yields 16'h100, but if you wrap just the addition: (8'hFF + 1) evaluates in an 8-bit context first → 8'h00 (overflow), then zero-extended to 16 bits. Precedence and width are coupled.
Classic Precedence Bugs Engineers Commit
Bug 1 — Mask Check Without Parentheses
logic [7:0] data = 8'hA5;
logic [7:0] mask = 8'hF0;
logic [7:0] exp = 8'hA0;
logic flag = 1'b1;
logic valid = 1'b1;
logic [7:0] sel_a = 8'h10;
logic [7:0] sel_b = 8'h20;
logic [7:0] offset = 8'h05;
logic [7:0] result;
// ── BUG 1: Bitwise mask check ──────────────────────────────────────
if (data & mask == exp) // BUGGY: data & (mask==exp) = data & 0 = 0
$display("upper nibble is A"); // never fires
if ((data & mask) == exp) // CORRECT
$display("upper nibble is A"); // fires
// ── BUG 2: Flag guard combined with bitwise ────────────────────────
if (valid & data == 8'hA5) // BUGGY: valid & (data==A5) = 1 & 1 = 1'b1 ← width 1!
$display("guarded check"); // fires by accident — valid & 1-bit result
if (valid && (data == 8'hA5)) // CORRECT: use logical &&
$display("guarded check"); // fires correctly
// ── BUG 3: Ternary with offset ────────────────────────────────────
result = valid ? sel_a : sel_b + offset; // BUGGY: valid ? sel_a : (sel_b+offset)
// valid=1: result = sel_a = 0x10 — offset never added!
result = (valid ? sel_a : sel_b) + offset; // CORRECT: 0x10 + 0x05 = 0x15
// ── BUG 4: NOT of comparison result ───────────────────────────────
if (!data == 8'h00) // BUGGY: (!data) == 8'h00 = 0 == 0 = 1 always!
$display("data is zero"); // fires even when data=0xA5
if (!(data == 8'h00)) // CORRECT: negate the equality result
$display("data is non-zero"); // fires when data != 0
// !data = logical not = 1'b0 when data is non-zero; then 0 == 8'h00 → always 1
// ── BUG 5: Shift in constraint arithmetic ─────────────────────────
logic [7:0] base = 8'd4;
result = base + 2 << 3; // BUGGY intent: base + (2<<3) = 4+16 = 20
// Parsed: (base + 2) << 3 = 6 << 3 = 48
result = base + (2 << 3); // CORRECT: 4 + 16 = 20Interview Questions
Beginner Level
Q1: How is a & b == c parsed in SystemVerilog? As a & (b == c). Equality == has higher precedence than bitwise AND & in SystemVerilog. This is the opposite of C, where & has higher precedence than ==. To get the intended (a & b) == c, you must add explicit parentheses. Q2: Is the conditional operator ?: left or right associative, and what does that mean for a ? b : c ? d : e? Right-associative. a ? b : c ? d : e is parsed as a ? b : (c ? d : e). The second ternary is the "false" branch of the first. This allows natural chaining: if a is true, return b; else evaluate the inner conditional. This is how priority chains are built with ternary operators.
Intermediate Level
Q3: What is the result of !data == 8'h00 when data = 8'hA5? 1 — and it fires even though data is not zero. Parsed as (!data) == 8'h00. Unary ! has higher precedence than ==. !8'hA5 = logical NOT of a non-zero value = 1'b0. Then 1'b0 == 8'h00 — the 1-bit 0 is zero-extended to 8'h00, which equals 8'h00 → result is 1. The correct form is !(data == 8'h00). Q4: In an RTL always block, what is the precedence risk in if (err_flag | timeout_flag && ~reset_n)?&& binds tighter than |, so this is parsed as err_flag | (timeout_flag && ~reset_n). If the intent was "either error flag or timeout flag, both gated by not-reset," the correct form is (err_flag | timeout_flag) && ~reset_n. As written, err_flag is not gated by reset at all — the error flag alone would trigger the block even during reset, which is typically a bug.
Experienced Engineer Level
Q5: A scoreboard has if (got & mask == expected) for checking masked fields. It passes all tests but the DUT has a field-packing bug. Why might this check never catch it? Because got & (mask == expected) is what executes. mask == expected evaluates first, producing a 1-bit 0 or 1. Then got & 1'b0 = 8'h00 or got & 1'b1 = got[0]. Either way, the masked comparison is never performed — the check tests one bit of got against 0 or 1, not the masked field against the expected value. The DUT can output any corrupted value in the masked field and this check will not catch it. Fix: if ((got & mask) == expected).
Best Practices — Writing Unambiguous Expressions
- Parenthesize any bitwise + equality mix — Any expression mixing &, |, ^ with == or != needs explicit parentheses. Always. No exception.
- Use && / || for boolean conditions — In if conditions combining multiple signals, use && and ||, not & and |. The precedence of logical operators vs bitwise differs and mixes unexpectedly.
- Parenthesize ternary when adding/shifting result — sel ? a : b + c is almost never what you want. If you need to operate on the ternary result, always wrap it: (sel ? a : b) + c.
- Don't rely on precedence — use parentheses — If you have to think about precedence when reading an expression, add parentheses. The compiler generates identical hardware. Future readers will not need to remember the table.
| Pattern | Write this | Not this |
|---|---|---|
| Masked field check | (data & mask) == expected | data & mask == expected |
| Guard condition | valid && (data == exp) | valid & data == exp |
| Add to ternary result | (sel ? a : b) + c | sel ? a : b + c |
| Negate a comparison | !(data == 8'h00) or data != 8'h00 | !data == 8'h00 |
| Multi-flag condition | (a_flag || b_flag) && enable | a_flag | b_flag && enable |
| Shift then compare | (data >> 4) == 4'hA | data >> 4 == 4'hA — relational binds tighter than shift? No — shift is above relational, so this is actually (data >> 4) == 4'hA correctly. But be explicit anyway. |
Summary — The Table You Actually Need to Memorize
You do not need to memorize all sixteen precedence levels. You need to memorize the three non-obvious rules that cause real bugs, and parenthesize everything else when mixing operator types.
| Rule | Counter-intuitive fact | Safe pattern |
|---|---|---|
| Equality above bitwise | == binds tighter than &, |, ^ — opposite of C | Always parenthesize: (a & b) == c |
| Logical AND above bitwise OR | && binds tighter than | | Use || / && for booleans; parenthesize mixed expressions |
| Conditional is very low | Almost everything binds tighter than ?: | Parenthesize ternary when it appears inside another expression |
- Unary operators first. Sign, logical NOT, bitwise NOT, reduction, increment — all bind before any binary operator.
- Arithmetic before shift before relational before equality. These follow standard mathematical intuition.
- Equality is above all bitwise binary operators. This is the SystemVerilog-specific trap. Memorize it.
- Bitwise AND → XOR → OR → Logical AND → Logical OR — that exact order from high to low within the bitwise/logical group.
- Conditional
?:is right-associative and near the bottom. Nested ternaries chain right-to-left naturally. - When in doubt, parenthesize. It costs nothing in hardware and everything in readability.