Operand widths resolve statically, before values compute. Most positions are context-determined: the widest participant, assignment target included, sets the width for all.
A few positions stay self-determined no matter the context. The shift amount (the right operand of << or >>), concatenation operands and the replication count keep their own width.
Position | Rule |
|---|---|
Shift amount (right operand of a shift) | Self-determined: never widens the shift |
Concatenation operands | Own width only; unsized literals illegal |
Replication count | Self-determined |
Value being shifted (left operand) | Context-determined: widened with the rest of the expression |
Comparison result | Self-determined single bit |
a and b are 8-bit and both hold 200. avg = (a + b) >> 1 is assigned to an 8-bit avg. What does avg hold?72, not 200. The context is 8 bits, so 400 wraps to 144 before the shift, and 144 halved is 72.
Averaging with (a+b)>>1 drops the carry before halving. With a = b = 200 the sum 400 wraps to 144, and the average reads 72 instead of 200.
The 8-bit target makes the whole context 8 bits, so the carry never exists. The shift cannot recover a carry the context never had: widen the context with a cast, a wider temp, or a 9-bit zero operand. With a 16-bit target the same (a+b)>>1 gives 200.
Unsized decimals are 32-bit signed, while sized literals are exact-width unsigned. The unbased fills '0 and '1 expand to whatever context they land in.
A 32-bit all-ones literal zero-extends into 64 bits. A literal wider than its size prefix truncates, often with only a lint note.
One block replays the averaging puzzle with all three fixes. A second block checks fill, extension and truncation.
module tb_avg_width;
timeunit 1ns; timeprecision 1ps;
logic [7:0] a, b;
logic [7:0] avg_bad, avg_ok1, avg_ok2, avg_ok3;
logic [8:0] sum;
int errors = 0;
initial begin
a = 8'd200;
b = 8'd200;
avg_bad = (a + b) >> 1;
avg_ok1 = (9'(a) + b) >> 1;
avg_ok2 = (a + b + 9'd0) >> 1;
sum = a + b;
avg_ok3 = sum >> 1;
#1;
if (avg_bad !== 8'd72) errors++;
if (avg_ok1 !== 8'd200) errors++;
if (avg_ok2 !== 8'd200) errors++;
if (avg_ok3 !== 8'd200) errors++;
if (sum !== 9'd400) errors++;
if (errors != 0) $fatal(1, "FAIL: averaging widths disagree");
$display("PASS: bad average is 72 while all three wide fixes give 200");
$finish;
end
endmodulePASS: bad average is 72 while all three wide fixes give 200module tb_literal_traps;
logic [63:0] mask64;
logic [15:0] r;
logic [39:0] big;
int errors = 0;
initial begin
mask64 = ~0;
if (mask64 !== 64'hFFFF_FFFF_FFFF_FFFF) errors++;
mask64 = 32'hFFFF_FFFF;
if (mask64 !== 64'h0000_0000_FFFF_FFFF) errors++;
mask64 = '1;
if (mask64 !== 64'hFFFF_FFFF_FFFF_FFFF) errors++;
r = 16'd70000;
if (r !== 16'd4464) errors++;
r = 1 << 16; // 1 is 32-bit, but the 16-bit target truncates 65536 to 0
if (r !== 16'h0000) errors++;
big = 1 << 35; // 40-bit context widens the shift operand, so 2^35 fits
if (big !== 40'h8_0000_0000) errors++;
if (errors != 0) $fatal(1, "FAIL: literal width checks disagree");
$display("PASS: 70000 truncates to 4464 and fill beats zero-extension");
$finish;
end
endmodulePASS: 70000 truncates to 4464 and fill beats zero-extensiona + b in a 9-bit sum first. Why does that need no cast?The 9-bit target makes the addition's context 9 bits, so 400 fits. Halving sum afterwards gives 200.
big = 1 << 35 works with a 40-bit target, yet r = 1 << 16 gives 0 with a 16-bit target. Why?The shift left operand is context-determined. The 40-bit context widens 1 to 40 bits before shifting, so 2^35 fits. The 16-bit case computes 65536 at 32 bits and truncates it to 0.
Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. Icarus 13 (-g2012) runs them too, with the same output.
Icarus 13 prints a truncation warning for 16'd70000; that truncation is what the bench checks.