Verilog bug catalog
Width Mismatches in Verilog: Why They Happen and How to Fix Them
Verilog and SystemVerilog will silently truncate, zero-extend, or sign-extend a signal whenever the right-hand side and the left-hand side of an assignment have different widths. The standard requires tools to allow it; the standard does not require them to warn. Width-mismatch bugs cost teams days of debug per project, especially when arithmetic carry bits get clipped. This page covers the patterns, the synthesis vs simulation behaviour, and how to scan a whole repository for them in one pass.
What Verilog actually does
IEEE 1800-2017 specifies that every assignment is performed at the width of the wider of the two sides ("context-determined" width), then the result is truncated or extended to the LHS width. The extension rule depends on signedness:
- Unsigned RHS narrower than LHS: zero-extended on the left.
- Signed RHS narrower than LHS: sign-extended (replicated MSB).
- RHS wider than LHS: truncated, top bits dropped silently.
- Mixed signed/unsigned in an expression: the expression is promoted to unsigned, then assigned. This is where surprises cluster.
Example: arithmetic truncation
The most common bug. A sum of two 8-bit values is 9-bit. Assigning it to an 8-bit register drops the carry. Hardware engineers know this in the abstract; everyone has shipped it at least once.
// BUG: 12-bit sum truncated into an 8-bit register. The carry bits
// are silently lost. The compiler does not warn by default.
module accum_bug (
input logic clk,
input logic [7:0] a,
input logic [7:0] b,
output logic [7:0] sum_q
);
always_ff @(posedge clk) begin
sum_q <= a + b; // (a+b) is 9-bit; LHS is 8-bit
end
endmoduleFix by widening the destination to fit the operation, or, if the truncation is intentional, write it explicitly so the next reader can see the intent.
// FIX: size the LHS to the operation, or explicitly truncate.
module accum_fixed (
input logic clk,
input logic [7:0] a,
input logic [7:0] b,
output logic [8:0] sum_q // widened to keep the carry
);
always_ff @(posedge clk) begin
sum_q <= {1'b0, a} + {1'b0, b};
end
endmoduleWhen the truncation is intentional, write it as 8'(a + b) or (a + b)[7:0] so the reviewer sees you meant it.
Example: signed/unsigned surprise
Mix a signed and an unsigned operand and the rules change. Verilog promotes the entire expression to unsigned, sign extension goes away, and the result is whatever you would get if you treated the signed value as if its top bit were just another data bit.
// BUG: signed/unsigned mismatch surprises the extension behaviour.
// 'a' is unsigned 8-bit, extended to 16-bit by zero-padding.
// 'b' is signed 8-bit, extended to 16-bit by sign-padding.
// 'a' + 'b' is then unsigned because of Verilog's promotion rules.
logic [7:0] a; // unsigned
logic signed [7:0] b; // signed
logic signed [15:0] r;
assign r = a + b;The defensive pattern is to make signedness explicit on every declaration, cast at the boundary with $signed() or $unsigned(), and never rely on implicit promotion in arithmetic.
Example: port connection mismatch
Width mismatches at port boundaries are particularly hard to spot because the declaration of the consumer is in a different file from the instantiation. The Verilog elaborator will quietly pad or truncate.
// BUG: port connection silently zero-extends a 4-bit signal into an
// 8-bit input. Synthesis builds it; simulation runs; the upstream
// engineer assumed the bus was 8-bit and sized everything else to match.
module child (input logic [7:0] data);
// ...
endmodule
module parent;
logic [3:0] nibble;
child u (.data(nibble)); // 4-bit -> 8-bit, top 4 bits = 0
endmoduleThe trick: the implicit-wire feature of Verilog will create a 1-bit wire if you typo a name in a port connection, so a typo at a port can become a width mismatch on top of a connectivity bug. Use `default_nettype none and .name connections to shut both classes of bug down at the source.
Example: a && b is always one bit
A width subtlety that bites even experienced engineers: the logical operators &&, ||, and ! always produce a single-bit result (1, 0, or x) no matter how wide their operands are: each side is first reduced to a boolean, then combined. The bitwise operators &, |, ~ instead keep the full operand width. Swap one for the other and you get a value of the wrong width with the wrong meaning — a width bug and a logic bug at once.
// BUG: bitwise '&' kept instead of logical '&&'. 'flags' is 8-bit, so
// 'flags & 8'h01' is an 8-bit value; assigning it to the 1-bit 'go'
// keeps only bit 0 -- but 'busy & ready' below is a per-bit AND of two
// 8-bit vectors, not the single-bit handshake the author intended.
logic [7:0] flags, busy, ready;
logic go, start;
assign go = flags & 8'h01; // probably meant: flags[0], or (flags != 0)
assign start = busy & ready; // 8-bit AND; meant: busy && ready (1 bit)Reach for the logical operator when you want a one-bit condition (a handshake, an enable, a guard) and the bitwise operator only when you genuinely mean a per-bit operation across a vector.
// FIX: '&&' / '||' / '!' always yield ONE bit (operands reduced to
// boolean first), which is exactly the handshake width you want. Use the
// bitwise operators only when you really mean a per-bit operation.
assign start = (busy != 0) && (ready != 0); // explicit, 1-bit result
assign go = flags[0]; // pick the bit you meantA type-aware analyzer has to model this rule explicitly, or it will either miss the bitwise-vs-logical mismatch or false-flag the intended one-bit width of a correct a && b. ChipVerify AI's analyzer evaluates the single-bit result of the logical operators correctly, so a clean handshake is not reported as a width mismatch.
Concatenation, replication, and explicit extension
The width of a concatenation is the sum of its element widths, and — as noted above — each element is self-determined. Sizing every element keeps the concatenation exactly the width you intend, and replication {N{...}} gives you a width-safe way to zero- or sign-extend by hand instead of relying on the implicit rule. Prefer the explicit form when the extension carries intent, such as widening a signed value.
// Concatenation width is the SUM of its (self-determined) parts. Sizing
// every element keeps the result the width you expect and prevents the
// LHS from truncating an oversized unsized literal.
logic [7:0] payload;
logic parity;
logic [8:0] framed;
assign framed = {parity, payload}; // 1 + 8 = 9 bits, exact
// Replication is explicit and width-safe for sign/zero extension:
logic signed [3:0] nib;
logic signed [7:0] ext;
assign ext = {{4{nib[3]}}, nib}; // manual sign-extend, no surprisesWhy simulation alone misses it
Simulation does not raise an error on a width mismatch. It performs the truncation or extension and continues. If your testbench never drives the high-order bits of the operands you are summing, the carry never gets clipped and the test passes. The bug surfaces in silicon when the application data has full range. Reviewers rarely spot the bug because the file looks reasonable: the LHS is the width the engineer expected, the RHS is an expression on signals of that width, and the arithmetic is correct. The bug is in the rule for what happens when an expression is one bit wider than its inputs.
How to detect it at scale
There are four production-grade detectors:
- Verilator: -Wall -Wwarn-WIDTH warns on every implicit width change. Treat it as fatal in CI: -Werror-WIDTH.
- Verible: forbid-implicit-truncation and the explicit-cast rules cover the source-level patterns at commit time, before any elaboration.
- Synthesis: Yosys's check pass and commercial synthesis tools all warn on width mismatch, but the warnings are buried in noisy logs and rarely make it back to the engineer who wrote the line.
- Type-aware AST analysis: walk every assignment, compare the elaborated bit-width of LHS and RHS, classify by severity (truncation vs extension vs port-connect).
Tools like ChipVerify AI's TinyTapeout scanner flag this rule as width_mismatch automatically. The output separates implicit truncation (loss of information, almost always a bug) from implicit extension (loss of intent, often a bug), so reviewers can triage quickly.
The self-determined vs context-determined distinction
A subtlety that surprises most engineers eventually: not every subexpression is sized by the surrounding context. The Verilog standard distinguishes self-determined operands (the count in a shift, the operands of relational and equality operators, the parts of a concatenation) from context-determined operands (the arms of a conditional, the operands of arithmetic operators). Self-determined expressions are evaluated at their natural width, ignoring the LHS, and only the result feeds the larger context.
logic [15:0] x;
logic [3:0] shift_amt;
logic [15:0] y;
// 'shift_amt' is self-determined: the shift count is computed at its own
// 4-bit width regardless of the 16-bit LHS. That is what you want.
assign y = x >> shift_amt;The trap appears in concatenations. Bits inside {...} are self-determined, so a literal {1, x} is not what you think: the 1 is a 32-bit unsized integer, the result is 32 bits wider than x, and the LHS truncates the top off. Always size literals inside concatenations: {1'b1, x}.
How to fix a width mismatch
Once a tool flags a width mismatch, the fix is almost always one of four moves — pick the one that matches your intent so the next reviewer can see what you meant:
- Widen the destination when the extra bits carry information. Size an accumulator to the operation: logic [8:0] sum_q; for an 8-bit + 8-bit add keeps the carry.
- Truncate explicitly when the narrowing is intentional. Write 8'(a + b) or (a + b)[7:0] so the loss is visible in review rather than silent.
- Make signedness explicit and cast at boundaries with $signed() / $unsigned() instead of relying on implicit promotion in mixed arithmetic.
- Size every literal and concatenation element — {1'b1, x}, not {1, x} — and use replication {{4{nib[3]}}, nib} for hand-controlled sign or zero extension.
For the broader picture of how lint, simulation, and structural checks combine to catch this class of bug before synthesis, see our guide to pre-signoff RTL verification.
Defensive coding practices
- Put `default_nettype none at the top of every file.
- Use named port connections (.data(data)) and avoid positional ones.
- When you mean to truncate, write the cast: SystemVerilog's 8'(expr) is the cleanest form.
- Size your accumulators wider than the operands. A common rule: log2(N) extra bits for an N-deep sum, one extra bit for any a + b.
- Run lint with width warnings as errors in CI.
Related rules
- Inferred latches — partial assignments to a wide bus can leave individual bits latched.
- Output never assigned — an implicit 1-bit wire created by a typo presents as a width mismatch on the receiver.
- Blocking vs non-blocking assignment — the other rule you should enforce in CI on every commit.
- Verible lint rules — the source-level forbid-implicit-truncation and explicit-cast checks that flag width bugs at commit time.
- Clock domain crossing — a width mismatch on a bus that also crosses clock domains compounds the risk, since the truncated bits are sampled asynchronously.
FAQ
Why does Verilog not warn about a width mismatch by default?
IEEE 1800 requires tools to accept assignments between operands of different widths and to truncate or extend automatically; it does not require a warning. Width warnings are also noisy on legacy code that relies on implicit padding to load integer literals into vectors, so most simulators leave them off by default. Enable the warning early on a new codebase (Verilator -Wwarn-WIDTH, promoted to -Werror-WIDTH in CI) and live with the noise on a brownfield one.
Why is a && b always one bit wide?
The logical operators &&, ||, and ! produce a single-bit result (1, 0, or x) regardless of operand width: each operand is first reduced to a boolean (non-zero is true), then combined. This differs from the bitwise &, |, ~ operators, which keep the operand width. Assigning a && b to a multi-bit vector zero-extends that single bit, which is usually what you want — but writing data & mask (bitwise) when you meant data && enable (logical), or vice versa, is a classic width-and-semantics bug. ChipVerify AI's analyzer models the 1-bit result of the logical operators correctly so it does not false-flag the intended single-bit width.
Are 32-bit unsized integer literals safe?
Almost never. A bare 0 or 1 in an expression is a 32-bit signed integer. Inside a concatenation it stays 32 bits wide, so {1, x} is 32 bits wider than x and the left-hand side silently truncates the top off. In an assignment to a narrow register it truncates. Always size literals: write 1'b1 inside concatenations and 8'd0 where the width matters.
Does a too-narrow assignment cause a latch?
No. Assigning a too-narrow expression to a whole vector zero-extends, sign-extends, or truncates the value — it does not latch. A latch only appears when an assignment is partial and path-dependent: some bits or indices are driven on some control paths and left unassigned on others. That is a separate rule, covered in the inferred-latches article.
How do I handle width with parameterised data widths?
Parameterise your accumulators too. If the input is DATA_W bits, the sum of two of them needs DATA_W+1 bits; an N-deep running sum needs DATA_W+$clog2(N). Put that arithmetic directly in the declaration, e.g. logic [DATA_W:0] sum_q, so the width tracks the parameter and the carry is never silently clipped.
Can ChipVerify AI prove my design has no width bugs?
No. ChipVerify AI is a pre-signoff structural analyzer, not a foundry signoff or an exhaustive proof. It reports the rule-matched implicit truncations, sign-extension surprises, and port-width mismatches it detects, with file-and-line evidence, so reviewers can triage them quickly. Use it alongside lint, simulation, and synthesis, not as a replacement for them.
Scan your repo
Scan your repo for width mismatches: paste a GitHub URL at chipverify.ai/tinytapeout and you'll get every implicit truncation, sign-extension surprise, and port-connection mismatch in the project, ranked by impact. No install, no Verilator setup, no lint config to tune.
Catch truncation and sign-extension bugs
Sign in and point ChipVerify AI at your RTL. Its analyzer reports implicit truncations, sign-extension surprises, and port-width mismatches with file-and-line evidence — a pre-signoff structural check, not a foundry signoff.