Part 1 · Language Foundations · Intermediate

Expression Width & Evaluation Rules

Context-determined vs self-determined width, intermediate truncation, the (a+b)>>1 overflow puzzle, and literal width traps.

Width is decided before evaluation

The width of every operand in a SystemVerilog expression is determined statically, before any value is computed . For most operators the rule is context-determined width: the width of the largest operand — including the assignment target — propagates to all operands, which are extended first and only then operated on. A few positions are self-determined : they keep their own natural width regardless of context. The crucial self-determined positions are the right operand of a shift, the operands inside a concatenation, and the replication count. Knowing which rule applies at each spot in an expression is the entire skill.

diagram
WIDTH PROPAGATION THROUGH AN ASSIGNMENT

  logic [8:0] y;
  logic [7:0] a, b;

  y = a + b;
      └──┬──┘
  step 1: context width = max(9, 8, 8) = 9   ← LHS participates!
  step 2: a, b zero-extended to 9 bits
  step 3: 9-bit addition  carry preserved  correct

  logic [7:0] z;
  z = (a + b) >> 1;
       └─┬─┘
  shift LEFT operand is its own context: max(8, 8, 8) = 8
   a+b computed in 8 bits  CARRY ALREADY LOST before the shift

The most important consequence: the assignment target's width flows backward into the right-hand side. The same expression a + b is computed at 9 bits when assigned to a 9-bit target and at 8 bits when assigned to an 8-bit one. Width is a property of the whole statement, not of the expression in isolation — which is why copy-pasting an expression into a different context can change its value.


The classic (a+b)>>1 averaging puzzle

Ask a candidate to average two 8-bit numbers with (a + b) >> 1 and watch for the trap: the addition's context is only the operands and the 8-bit result needed by the shift, so the ninth carry bit is discarded before the shift halves the value. For a = b = 200, the true sum 400 wraps to 144 and the "average" comes out as 72 instead of 200. The fix is to force a wider context for the addition — a cast, a wider intermediate variable, or the cheap idiom of adding a known-wider zero operand.

systemverilog
logic [7:0] a = 8'd200, b = 8'd200;
logic [7:0] avg_bad, avg_ok1, avg_ok2, avg_ok3;

initial begin
  // BUG: addition context is 8 bits → 200+200 = 400 wraps to 144
  avg_bad = (a + b) >> 1;              // 72, expected 200

  // FIX 1: explicit width cast widens the addition context
  avg_ok1 = (9'(a) + b) >> 1;          // 9-bit add → 400 → 200

  // FIX 2: a 9-bit zero operand widens the whole context
  avg_ok2 = (a + b + 9'd0) >> 1;       // 200

  // FIX 3: wider intermediate makes the width explicit and readable
  logic [8:0] sum;
  sum     = a + b;                     // LHS width 9 → 9-bit add
  avg_ok3 = sum >> 1;                  // 200
end

Note why the shift does not help: the right operand of a shift is self-determined and the shift result takes the width of its left operand — so wrapping the addition in a shift adds no bits anywhere. Comparisons behave similarly: each side of < is widened to the larger side, but the comparison result is a self-determined single bit, so nothing outside the comparison widens its operands.


Literal width traps

Integer literals have their own width rules. An unsized decimal like 42 is at least 32 bits and signed. A sized literal like 4'hF is exactly that width and unsigned. The unbased unsized literals '0, '1, 'x, 'z expand to fill their context — the only literals that are truly width-adaptive. Two traps follow: a sized literal that is narrower than the data it must mask truncates silently (8'hFFF is just 8'hFF plus a lint warning at best), and an unsized 32-bit literal in a 64-bit context zero-extends rather than sign-extends if written without care.

systemverilog
logic [63:0] mask64;
logic [15:0] r;

initial begin
  mask64 = ~0;          // ~0 evaluated at 64-bit context → all ones: OK
  mask64 = 32'hFFFF_FFFF; // zero-extends → upper 32 bits are 0!
  mask64 = '1;           // unbased unsized → fills 64 bits: the safe idiom

  r = 16'd70000;         // literal exceeds 16 bits → truncates to 4464
  r = 1 << 16;           // 1 is 32-bit → OK here, but:
  // logic [39:0] big = 1 << 35;  // also fine: 1 is 32-bit but the
  // assignment context is 40 bits and propagates into the shift's
  // left operand — context-determined widening saves this case.
end

Interview angle

  • "Why does (a+b)>>1 give the wrong average?" — the carry is truncated before the shift; widen the addition context.

  • "Does the LHS affect RHS evaluation width?" — yes, the assignment target participates in context-determined width.

  • "Which operands are self-determined?" — shift RHS, concat operands, replication counts, comparison results.

Key takeaways

  • Width is resolved statically before evaluation; the assignment target widens the right-hand side.

  • Self-determined positions (shift RHS, concat operands) ignore context — know the short list.

  • (a+b)>>1 truncates the carry before shifting; widen with a cast, wider temp, or 9'd0 operand.

  • '0 and '1 are the only context-filling literals; sized and 32-bit literals zero-extend or truncate silently.

Common pitfalls

  • Averaging with (a+b)>>1 — overflow truncates before the shift halves.

  • Assigning 32'hFFFF_FFFF to a 64-bit variable expecting all ones — upper half is zero; use '1.

  • Writing a literal wider than its size prefix (8'hFFF) — silent truncation, often only a lint note.

  • Assuming an expression has one fixed value — the same RHS evaluates differently under different target widths.