SystemVerilog · Module 4
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 | < <= > >= inside dist | Relational, set membership, distribution | Left-to-right | Below shift. inside belongs here, at relational level — not below the logical operators |
| 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, so a | b && c = (a | b) && c |
| 13 | || | Logical OR | Left-to-right | Below logical AND |
| (see below) | — | — | inside is not here — corrected into row 7 | |
| 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'hFF && 1 = 1'b1 |
a | (b && c) | Force the logical AND first | 8'hF0 | (8'h0F && 1) = 8'hF0 | 1'b1 = 8'hF1 |
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. Width is determined separately, by SystemVerilog's expression-sizing rules, and the two interact — but not in the way it is often assumed.
Parentheses do not create a sizing context. This is the single most useful thing to know here, and it is the opposite of what many engineers believe. Grouping changes which operands an operator receives; it does not isolate a sub-expression from the width of the surrounding assignment.
logic [15:0] r1, r2;
assign r1 = 8'hFF + 1 ; // 16'h0100
assign r2 = (8'hFF + 1); // 16'h0100 -- identical. The parentheses
// change nothing about the width.Both are 16'h0100. The addition is a context-determined operand of the assignment, so it is evaluated at the width of the whole expression — 16 bits — and 8'hFF + 1 does not overflow there. Adding parentheses around it changes the parse tree in a way that has no effect, because the grouping was already what the parentheses express.
What does truncate is a construct that genuinely establishes a self-determined width. A concatenation is the common one:
logic [15:0] r3;
assign r3 = {8'hFF + 1}; // 16'h0000 -- the concatenation makes the
// addition self-determined at 8 bits, so it
// wraps to 8'h00, then zero-extends.That is the real hazard, and it has nothing to do with precedence: {} is a sizing boundary, () is not. Precedence and width are coupled only in the sense that regrouping can change which operands feed an operator, and therefore what the self-determined width of a sub-expression is. Never assume a pair of parentheses will contain an overflow. See Concatenation, Replication & Conditional for the sizing rules the braces impose.
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
Proving It — A Self-Checking Precedence Testbench
Every grouping claim on this page is checkable in about thirty seconds. Paste this into any simulator; it asserts rather than prints, so a wrong claim fails loudly instead of scrolling past.
// precedence_proof.sv
//
// Self-checking proof of the groupings this chapter asserts. Each check
// compares the bare expression against the explicitly-parenthesized form it
// is CLAIMED to be equivalent to. If a claim on this page were wrong, the
// corresponding assertion would fail here.
module precedence_proof;
int errors = 0;
task automatic check(string name, logic [31:0] got, logic [31:0] exp);
if (got !== exp) begin
errors++;
$display("FAIL %-42s got=%0h exp=%0h", name, got, exp);
end else
$display("pass %-42s = %0h", name, got);
endtask
initial begin
logic [7:0] a, b, data, mask, got, expected;
logic c, valid;
logic [15:0] r1, r2, r3;
// ---- 1. Equality binds tighter than binary bitwise AND ------------
// a & b == c == a & (b == c)
a = 8'hF0; b = 8'h0F; c = 1'b0;
check("a & b == c == a & (b==c)",
a & b == c, a & (b == c));
// ...and is NOT the same as the grouping people intend:
if ((a & b == c) === ((a & b) == c))
$display("NOTE: the two groupings coincide for these operands - "
, "pick different ones to see them diverge");
// ---- 2. Binary | binds tighter than && ---------------------------
// a | b && c == (a | b) && c <-- NOT a | (b && c)
a = 8'hF0; b = 8'h0F; c = 1'b1;
check("a | b && c == (a|b) && c",
a | b && c, (a | b) && c);
// Demonstrate that the OTHER grouping gives a different value, so the
// check above is not passing by coincidence:
// (a|b) && c = 8'hFF && 1 = 1'b1
// a | (b&&c) = 8'hF0 | 1 = 8'hF1
check("a | (b && c) differs (expect 8'hF1)",
a | (b && c), 8'hF1);
// ---- 3. Unary ! binds tighter than == ----------------------------
data = 8'hA5;
check("!data == 8'h00 == (!data) == 8'h00",
!data == 8'h00, (!data) == 8'h00);
check("!data == 8'h00 is 1 for NONZERO data",
!data == 8'h00, 1'b1); // the trap, proven
// ---- 4. Shift binds looser than + --------------------------------
a = 8'd4;
check("a + 2 << 3 == (a+2) << 3",
a + 2 << 3, (a + 2) << 3);
check("a + 2 << 3 is 48, not 20",
a + 2 << 3, 32'd48);
// ---- 5. ?: is right-associative ----------------------------------
check("a?b:c?d:e == a?b:(c?d:e)",
1'b0 ? 8'd1 : 1'b1 ? 8'd2 : 8'd3,
1'b0 ? 8'd1 : (1'b1 ? 8'd2 : 8'd3));
// ---- 6. Ternary binds looser than + ------------------------------
valid = 1'b1;
check("sel ? a : b + c == sel ? a : (b+c)",
valid ? 8'd5 : 8'd3 + 8'd10,
valid ? 8'd5 : (8'd3 + 8'd10));
// ---- 7. Parentheses are NOT a sizing context ---------------------
r1 = 8'hFF + 1 ;
r2 = (8'hFF + 1);
r3 = {8'hFF + 1};
check("(8'hFF+1) equals 8'hFF+1 - parens do not truncate", r2, r1);
check("8'hFF+1 in 16-bit context is 16'h0100", r1, 16'h0100);
check("{8'hFF+1} IS self-determined - truncates to 0", r3, 16'h0000);
// ---- 8. The checker bug from Q5 ----------------------------------
got = 8'h5A; mask = 8'hF0; expected = 8'h50;
check("got & mask == expected == got & (mask==expected)",
got & mask == expected, got & (mask == expected));
check("the INTENDED form is true here",
(got & mask) == expected, 1'b1);
check("the BUGGY form is false here",
got & mask == expected, 32'd0);
if (errors == 0) $display("\nprecedence_proof: ALL CHECKS PASSED");
else $display("\nprecedence_proof: %0d FAILURES", errors);
$finish;
end
endmoduleCheck 2 is the one worth running yourself. It proves both that a | b && c groups as (a | b) && c, and — via the second assertion — that the other grouping yields a genuinely different value, so the first check is not passing by accident on convenient operands. That distinction matters: a proof that would pass under either grouping proves nothing.
A scoreboard passed for eight months because its masked comparison never compared anything
CHECKER-STOPPED-CHECKING// ❌ BUG: a field checker in a UVM scoreboard. Reads correctly in review.
// Parses as got & (FIELD_MASK == expected_field).
task automatic check_field(logic [31:0] got, logic [31:0] expected_field);
if (got & FIELD_MASK == expected_field)
`uvm_info("SB", "field matched", UVM_HIGH)
else
`uvm_error("SB", $sformatf("field mismatch: got %0h exp %0h",
got & FIELD_MASK, expected_field))
endtask
// What actually executes:
// FIELD_MASK == expected_field -> a 1-BIT value, 0 or 1
// got & 1'b0 -> 32'h0 -> else branch, always
// got & 1'b1 -> got[0] -> depends on ONE bit
//
// Note the error message: it prints `got & FIELD_MASK`, correctly masked.
// The message shows the right thing. The CONDITION tests something else.
// ✅ FIX: parenthesize the mask, and compare the masked value.
task automatic check_field(logic [31:0] got, logic [31:0] expected_field);
if ((got & FIELD_MASK) == expected_field)
`uvm_info("SB", "field matched", UVM_HIGH)
else
`uvm_error("SB", $sformatf("field mismatch: got %0h exp %0h",
got & FIELD_MASK, expected_field))
endtaskA packet-field scoreboard had been green for eight months across every regression. A field-packing bug reached silicon: one 8-bit field in a 32-bit descriptor was being written one nibble off, and nothing in the environment had reported it.
The scoreboard had a check for exactly that field. It was in the coverage report. It had executed roughly four million times.
if (got & FIELD_MASK == expected_field) parses as if (got & (FIELD_MASK == expected_field)), because equality binds tighter than binary bitwise AND.
Trace what that computes. FIELD_MASK is a constant — say 32'h0000_FF00. expected_field is whatever the reference model produced. They are almost never equal, so FIELD_MASK == expected_field is 1'b0, and got & 1'b0 is zero. The condition is false on essentially every call, so the else branch runs every time — and the else branch is the one that reports a mismatch.
Which raises the question the team asked first: if the else branch always runs, why was there never an error? Because of the second half of the bug. The scoreboard was comparing got & FIELD_MASK against expected_field in the message only, and the surrounding code had been written so that this task was called from a path that treated uvm_error as informational during a bring-up phase, and the demotion was never removed. The precedence bug made the check meaningless; a stale severity demotion made its output invisible.
Either defect alone would have been caught. Together they produced a check that ran four million times and could not report anything.
The precedence half is the one worth generalising. A checker with a precedence bug does not fail loudly — it stops discriminating. The condition becomes a constant, or a function of one unrelated bit, and the result is a test that passes for a reason unconnected to the design. Nothing in a pass/fail regression can distinguish that from a design that is genuinely correct.
The message string is the cruel detail. got & FIELD_MASK appears in the $sformatf, correctly parenthesized by the argument boundary, so anyone reading the code sees a correct masked comparison somewhere in the task and moves on.
Parenthesize, then make the class of bug detectable rather than relying on review.
// 1. The immediate fix.
if ((got & FIELD_MASK) == expected_field)
// 2. The guard that finds the whole class. A comparison that can never be
// false has an uncovered failing branch - and that IS visible in a
// coverage report, unlike a passing test.
covergroup cg_field_check;
cp_result : coverpoint field_matched {
bins matched = {1'b1};
bins mismatched = {1'b0}; // zero hits after a full regression
} // means this check never discriminated
endgroup
// 3. The structural habit: compute the masked value into a named variable
// FIRST. The precedence question then cannot arise, and the intent is
// readable without knowing the table.
logic [31:0] masked = got & FIELD_MASK;
if (masked == expected_field) ...Three habits follow, in increasing order of how much they buy:
- Parenthesize any expression mixing precedence families. Bitwise with equality, logical with bitwise, arithmetic with shift. Being sure of the table is not a reason to omit them — the next reader is not sure, and the reviewer after that is reading a diff.
- Name the intermediate.
masked = got & FIELD_MASK;on its own line removes the question entirely, and it is what the error message was already doing. - Cover the failing branch of every checker. This is the only one that works when the review misses it. A
mismatchedbin at zero hits after a full regression is the signature of a check that has stopped checking, whatever the cause — precedence, a demoted severity, a filtered message, or a guard that is never true.
That last point generalises past this chapter: an assertion or checker that has never failed has never been shown to work. Somewhere in the suite, something should deliberately break the thing it watches.
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.
Related Pages & References
The operator families this page orders. For what each group actually computes, and where the two headline traps come from: Bitwise Operators (&, |, ^, ~ — per-bit, full width) and Logical Operators (&&, ||, ! — truth values, 1 bit). Confusing the two is the reason the bitwise-below-equality and bitwise-above-logical boundaries bite. Reduction Operators covers the unary forms of the same symbols, which sit at the top of the table rather than the middle — &a & b is a reduction of a ANDed with b, not a double bitwise AND.
For the operands: Shift Operators (below arithmetic, which is why a + 2 << 3 is (a+2) << 3), Arithmetic Operators, Relational & Equality Operators, and Concatenation, Replication & Conditional — the last of which owns the sizing rules that make {} a truncation boundary while () is not.
References.
- IEEE 1800 (SystemVerilog) — the normative precedence and associativity table is in the operators-and-expressions clause. Two placements on this page were corrected against it:
insidesits with the relational operators, not below the logical ones; and binary|sits above&&, so a mixed expression groups the bitwise part first. The expression-sizing rules that make a concatenation's operands self-determined, while parentheses are not a sizing context, are in the same clause. - IEEE 1364 (Verilog) — the same precedence ordering, inherited unchanged. Nothing on this page differs between the two.
A note on the C comparison, since this page previously got it backwards. C's precedence also places equality above bitwise AND — a & b == c groups as a & (b == c) in C exactly as it does here. The two languages agree. What is surprising is the ordering itself, in both languages, and an engineer arriving from C who has met this trap before already has the correct instinct rather than a misleading one.
Requirement versus style. Everything in the precedence table is a language requirement. Everything in the Best Practices section — parenthesize across families, name intermediates, cover the failing branch — is style. The distinction matters here more than usual: the table tells you what the compiler will do, and the style rules exist because being able to predict the compiler is not the same as making the next reader able to.
Part of SystemVerilog Fundamentals·Operators & Expressions·Lesson 28 of 53
View program