Part 1 · Language Foundations · Intermediate
Signed & Unsigned Arithmetic
Sign extension rules, mixed signed/unsigned expression poisoning, $signed/$unsigned casts, and the classic signed-vs-unsigned compare bug.
How the simulator decides signedness
Every expression in SystemVerilog has a signedness computed before evaluation . The rule from the LRM is brutal in its simplicity: an arithmetic or comparison context is signed only if every operand is signed . A single unsigned operand — a plain logic [7:0] net, an unsized literal like '1, a sized unsigned literal — converts the entire context to unsigned. This is called expression poisoning : the unsigned operand poisons every signed operand around it, and negative values are silently reinterpreted as large positive numbers.
Sign extension follows from signedness. When a byte or logic signed [7:0] value widens to fit a larger context, the simulator replicates the MSB (the sign bit) into the new upper bits. An unsigned value zero-extends instead. The extension decision is made per-operand after the context signedness is resolved — so a signed operand inside a poisoned (unsigned) expression zero-extends , which is exactly where the wrong answers come from.
SIGNEDNESS RESOLUTION (per expression context)
operand A operand B context result
───────── ───────── ──────────────
signed + signed → SIGNED (sign-extend both)
signed + unsigned → UNSIGNED (zero-extend BOTH!)
unsigned + unsigned → UNSIGNED
Example: byte s = -1 (8'hFF), logic [15:0] u = 16'h0010
s < u → context UNSIGNED
→ s zero-extends: 16'h00FF = 255
→ 255 < 16 → FALSE (you expected -1 < 16 → TRUE)The classic compare bug
The single most common signedness bug — and a favorite interview question — is comparing a signed counter against an unsigned bound. The intent reads correctly; the simulation does not. The fix is to make every operand signed, either by declaring it so or by casting with $signed().
module signed_compare_bug;
logic signed [7:0] temperature = -8'sd5; // signed: -5
logic [7:0] threshold = 8'd10; // unsigned: 10
initial begin
// BUG: mixed signed/unsigned → unsigned context.
// -5 reinterpreted as 8'hFB = 251, and 251 < 10 is FALSE.
if (temperature < threshold)
$display("below threshold"); // never prints!
else
$display("BUG: 251 < 10 is false");
// FIX 1: cast the unsigned operand into the signed world
if (temperature < $signed({1'b0, threshold}))
$display("FIX: -5 < 10 correctly true");
// FIX 2: declare both operands signed at the source
// logic signed [7:0] threshold = 8'sd10;
end
endmoduleNote the {1'b0, threshold} in the fix: casting an unsigned 8-bit value directly with $signed(threshold) would reinterpret 8'hFB as -5, which is wrong if the value is a genuine magnitude above 127. Prepending a zero bit guarantees the cast preserves the unsigned value. $signed and $unsigned never change bits — they only change how the bits are interpreted for extension and comparison.
Where poisoning hides in real code
Poisoning rarely appears as a clean two-operand compare. It hides inside larger expressions: a literal without an s suffix, a concatenation (concatenation results are always unsigned regardless of operands), a part-select (also always unsigned), or a function returning an unsigned type. Any one of these inside an otherwise-signed expression flips the whole context.
logic signed [15:0] a, b;
// Literal without 's' suffix is unsigned → poisons the context
logic ok1 = (a > -16'sd4); // signed compare: correct
logic bad = (a > -4); // -4 is a signed integer literal: OK
logic bad2 = (a > 16'hFFFC); // unsigned literal → unsigned compare!
// Part-selects and concatenations are ALWAYS unsigned
logic signed [7:0] s8;
logic r1 = (s8[7:0] < 0); // part-select is unsigned → never true
logic r2 = ({s8} < 0); // concat is unsigned → never true
logic r3 = (s8 < 0); // plain signed variable → works
// Arithmetic right shift requires a signed operand (see shifts lesson)
logic signed [15:0] q = a >>> 2; // sign-replicating shift
logic [15:0] u = a >> 2; // logical shift, zero fillInterview angle
"Why does my signed comparison fail?" — explain poisoning: one unsigned operand makes the context unsigned.
"What does $signed actually do?" — reinterprets bits for extension/compare; it never modifies them.
"Is a part-select signed if the vector is signed?" — no, part-selects and concatenations are always unsigned.
Key takeaways
Expression signedness is resolved before evaluation: all operands signed, or the context is unsigned.
A poisoned context zero-extends signed operands — negative values become large positives.
$signed/$unsigned change interpretation, not bits; widen unsigned values with a leading 0 before $signed.
Part-selects, concatenations, and unsized based literals are always unsigned.
Common pitfalls
Comparing signed counter < unsigned bound — the comparison silently goes unsigned and fails for negatives.
Assuming 16'hFFFC means -4 in a signed context — sized hex literals are unsigned without the s suffix.
Calling $signed(u8) on a value that may exceed 127 — reinterprets the MSB as a sign bit.
Trusting a part-select of a signed vector to stay signed — it never does.