Skip to content

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 writtenHow SV parses itWhat engineer intended
a & b == ca & (b == c)== binds tighter than &(a & b) == c
a | b && c(a | b) && c| binds tighter than &&a | (b && c)
sel ? a : b + csel ? 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).

LevelOperatorsTypeAssociativityNotes
1 (Highest)( )   [ ]   { }   {{ }}Grouping / index / concat / replicateExplicit grouping always wins
2+   -   !   ~   &   ~&   |   ~|   ^   ~^   ^~   ++   --Unary operators (sign, logic, reduction, inc/dec)Right-to-leftAll unary ops — same level, right-associative
3**ExponentiationRight-to-left2**3**2 = 2**(3**2) = 512
4*   /   %Multiply, divide, moduloLeft-to-rightStandard arithmetic priority
5+   -Binary add, subtractLeft-to-rightLower than multiply
6<<   >>   <<<   >>>Shift operatorsLeft-to-rightBelow arithmetic — a + b << 2 = (a+b) << 2
7<   <=   >   >=   inside   distRelational, set membership, distributionLeft-to-rightBelow shift. inside belongs here, at relational level — not below the logical operators
8==   !=   ===   !==   ==?   !=?Equality / case equality / wildcard equalityLeft-to-rightHigher than bitwise — the C-programmer trap
9&Binary bitwise ANDLeft-to-rightBelow equality — a & b == c = a & (b==c)
10^   ~^   ^~Binary bitwise XOR / XNORLeft-to-rightBelow bitwise AND
11|Binary bitwise ORLeft-to-rightBelow XOR — AND | XOR | OR from top to bottom
12&&Logical ANDLeft-to-rightBelow bitwise OR, so a | b && c = (a | b) && c
13||Logical ORLeft-to-rightBelow logical AND
14(see below)inside is not here — corrected into row 7
15? :Conditional (ternary)Right-to-leftVery low — sel ? a : b + c = sel ? a : (b+c)
16 (Lowest)=   +=   -=   *=   /=   %=   &=   |=   ^=   <<=   >>=Assignment operatorsRight-to-leftAlways last — entire RHS evaluates before assignment

Step-by-Step Evaluation — Seeing Precedence in Action

The Classic Bitwise-vs-Equality Trap

ExpressionStep 1 (higher prec first)Step 2Final result
data & 8'hF0 == 8'hA08'hF0 == 8'hA01'b0 (equality first)data & 1'b08'h008'h00 — wrong!
(data & 8'hF0) == 8'hA0data & 8'hF0 → masked datamasked == 8'hA0 → 0 or 1Correct boolean

Logical vs Bitwise Mixing Trap

ExpressionParsed asResult (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 first8'hF0 | (8'h0F && 1) = 8'hF0 | 1'b1 = 8'hF1

Conditional Trap — Lower Operand Captured

ExpressionParsed asIf sel=1, a=5, b=3, c=10
sel ? a : b + csel ? a : (b + c)sel=1 → result = a = 5
(sel ? a : b) + cTernary first, then add csel=1 → (a) + c = 5 + 10 = 15

Shift and Arithmetic Interaction

ExpressionParsed asWith 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 add2 + (3 << 2) = 2 + 12 = 14

Code Examples — Precedence Traps in Practice

Example 1 — The Bitwise Mask Check Bug

Precedence Trap 1 — & vs ==
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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         = A0

Example 2 — Conditional and Arithmetic Precedence

Precedence Trap 2 — Conditional vs Arithmetic
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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  = 13

Example 3 — Negation and Reduction Operator Interaction

Precedence Trap 3 — Unary Negation vs Reduction
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
 
endmodule

Example 4 — Full Verification: All Traps in One Testbench

Example 4 — Comprehensive Precedence Validation
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
 
endmodule

How 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.

ExpressionAssociativityGrouped asNote
a - b - cLeft(a - b) - cStandard math grouping
a && b && cLeft(a && b) && cShort-circuits left to right
2 ** 3 ** 2Right2 ** (3 ** 2) = 512NOT (2**3)**2 = 64
a ? b : c ? d : eRighta ? b : (c ? d : e)Nested ternary chains naturally
a = b = cRighta = (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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

Bug Collection — All Classic Precedence Traps
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 = 20

Interview 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endmodule

Check 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.

Precedence families in order: unary and arithmetic, then shift, then relational and inside, then equality, then binary bitwise AND XOR OR, then logical AND and OR, then conditional and assignment. Equality above bitwise and bitwise above logical are the two boundaries that cause most bugs.Unary, **, * / %, + -binds firstShift << >> <<< >>>below arithmeticRelational, insidebelow shiftEquality == != ===!==below relationalBitwise & then ^ then|BELOW equalityLogical && then ||BELOW bitwiseTrap 1: maskedcomparea & b == c groups wrongTrap 2: | above &&reverse of intuition?: then assignmentbinds last12
Figure 1 - the precedence families, from tightest to loosest, and the two boundaries that cause almost every real bug. Unary and arithmetic bind first, then shift, then relational and inside, then equality. Below equality come the three binary bitwise operators, and below those the two logical operators. The first trap is that equality binds tighter than bitwise, so a masked comparison groups the wrong way. The second is that bitwise OR binds tighter than logical AND, which is the reverse of most engineers' intuition.
1

A scoreboard passed for eight months because its masked comparison never compared anything

CHECKER-STOPPED-CHECKING
Buggy Code & Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ 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))
endtask
Symptom

A 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.

Root Cause

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.

Fix

Parenthesize, then make the class of bug detectable rather than relying on review.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 mismatched bin 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.

PatternWrite thisNot this
Masked field check(data & mask) == expecteddata & mask == expected
Guard conditionvalid && (data == exp)valid & data == exp
Add to ternary result(sel ? a : b) + csel ? a : b + c
Negate a comparison!(data == 8'h00) or data != 8'h00!data == 8'h00
Multi-flag condition(a_flag || b_flag) && enablea_flag | b_flag && enable
Shift then compare(data >> 4) == 4'hAdata >> 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.

RuleCounter-intuitive factSafe pattern
Equality above bitwise== binds tighter than &, |, ^ — opposite of CAlways parenthesize: (a & b) == c
Logical AND above bitwise OR&& binds tighter than |Use || / && for booleans; parenthesize mixed expressions
Conditional is very lowAlmost 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.

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: inside sits 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

Continue learning