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.
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 shiftThe 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.
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
endNote 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.
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.
endInterview 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.