Skip to content

SystemVerilog · Module 4

Shift Operators

<<, >>, <<<, >>> — logical vs arithmetic right shift, signed behavior, sign extension.

Module 4 · Page 4.6

The One Operator That Actually Has a Trap

If you are verifying a DSP core, a fixed-point multiplier, or any design that divides signed values by powers of two, shift operators become genuinely important. The arithmetic right shift (>>>) is what makes signed_val >>> 1 equivalent to dividing by 2 while preserving the sign. The logical right shift (>>) shifts in zeros from the left — which is correct for unsigned values but gives the wrong answer for negative signed values.

The concrete failure: on an 8-bit signed variable, -8 >> 1 gives +124. -8 is 8'b1111_1000; shifting right by one and filling with zero gives 8'b0111_1100, which read as a signed 8-bit value is +124. -8 >>> 1 gives -4, the correct arithmetic result. Both operations succeed silently — no error, no warning, just a wrong number feeding the rest of the datapath.

Notice what kind of wrong number it is. +124 is not a wild value that a range check would catch; it is a plausible byte. Sign errors in shifted data tend to surface as an accumulator that drifts, or a filter whose output is asymmetric about zero, long after the shift itself.

Left shifts have no such distinction — both << and <<< always shift in zeros on the right. They are functionally identical. The double-angle form <<< exists for symmetry with the arithmetic right shift, but adds nothing different.

Four Operators, Two Distinctions

OperatorNameFill directionFill valueUse for
<<Logical left shiftFills right (LSB side)Always 0Multiply by 2^n, bit-mask generation
<<<Arithmetic left shiftFills right (LSB side)Always 0 — identical to <<Signed multiply by 2^n (same result as <<)
>>Logical right shiftFills left (MSB side)Always 0Unsigned divide by 2^n, extract upper bits
>>>Arithmetic right shiftFills left (MSB side)Sign bit for signed types, 0 for unsignedSigned divide by 2^n — preserves sign

How Arithmetic Right Shift Works

When you right-shift a signed negative number, the vacated MSB positions need to be filled. Logically shifting in 0s turns the number positive — which is arithmetically wrong. Arithmetically shifting in copies of the sign bit (1 for negative, 0 for positive) preserves the sign and gives the correct floor-division-by-power-of-two result.

Logical left shift

<< shifts bits toward the MSB and fills zeros on the right — a multiply by 2 per position. Bits shifted past the MSB are lost, which silently changes the sign of a signed value. Identical in every case to <<<.

Logical right shift

Shifts bits toward LSB, fills 0s on the left. Correct for unsigned divide-by-2. Wrong for signed negative — turns negative into large positive.

Arithmetic right shift

Fills with the sign bit (MSB). For negative signed values, fills 1s. For positive or unsigned, fills 0s. This is what you want for signed division.

Arithmetic left shift

<<< fills zeros on the right — identical to << in all cases. It exists for symmetry with >>>, not because left shifting has an arithmetic variant. Either form is safe.

Syntax & Type Rules

SystemVerilog — Shift Operator Syntax
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// General syntax: operand shift_op shift_amount
result = value <<  n;    // logical  left  — n can be an expression
result = value >>  n;    // logical  right
result = value <<< n;    // arith    left  — same as <<
result = value >>> n;    // arith    right — sign-extends if value is signed
 
// ── Unsigned examples (logic [7:0]) ──────────────────────────────
logic [7:0] u = 8'b0001_0000;   // 16
u <<  2;   // 8'b0100_0000 = 64  (×4)
u >>  2;   // 8'b0000_0100 = 4   (÷4, zero-fill)
u >>> 2;   // 8'b0000_0100 = 4   (same — unsigned)
 
// ── Signed examples (logic signed [7:0]) ─────────────────────────
logic signed [7:0] s = -8;      // 8'b1111_1000
s >>  2;    // 8'b0011_1110 = +62  WRONG: shifted in 0s
s >>> 2;    // 8'b1111_1110 = -2   CORRECT: shifted in sign bit (1)
 
// ── Shift amount rules ───────────────────────────────────────────
// Shift amount is always treated as UNSIGNED
// If shift amount >= operand width, result is 0 (or all sign bits for >>>)
logic [7:0] v = 8'hFF;
v >> 8;    // 8'h00 — shifted out entirely
v >> 100;  // 8'h00 — same
 
// ── Variable shift (generates barrel shifter in synthesis) ───────
logic [3:0] shift_amt;
result = data << shift_amt;   // shift_amt computed at runtime

Type Determines >>> Behavior

DeclarationType>>> behavior
logic [7:0] vUnsigned 4-stateZero-fill — same as >>
logic signed [7:0] vSigned 4-stateSign-extend — fills MSB copies
int vSigned 2-state 32-bitSign-extend
bit [7:0] vUnsigned 2-stateZero-fill
shortint vSigned 2-state 16-bitSign-extend
$signed(v) >>> nCast to signed contextSign-extend regardless of original declaration

Step-by-Step Visual Evaluation

Left Shifts — Bit Movement Diagram

Starting value: 8'b0001_1010 = 26

OperationBit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0Decimal
Original0001101026
<< 10011010052 (×2)
<< 201101000104 (×4)
<< 311010000208 (×8, fits in 8 bits)
<< 410100000160 — MSB of original lost

Green cells = zeros shifted in from the right.

Right Shifts — Logical vs Arithmetic on -8

logic signed [7:0] s = -8 → binary 8'b1111_1000. Watch what fills in from the left:

OperationBit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0ValueCorrect?
Original (-8)11111000-8
>> 1 (logical)01111100+124❌ Wrong
>>> 1 (arithmetic)11111100-4✅ Correct (-8/2)
>> 2 (logical)00111110+62❌ Wrong
>>> 2 (arithmetic)11111110-2✅ Correct (-8/4)

Green = zero fill (logical). Orange = sign-bit fill (arithmetic).

Shift Amount Effects

Shift amountEffect on 8'hFF >> nEffect on signed -8 >>> n
08'hFF (unchanged)-8 (unchanged)
18'h7F = 127-4
48'h0F = 15-1 (all sign bits)
78'h01 = 1-1 (still all sign bits)
88'h00 = 0 (fully shifted out)-1 (sign bit fills entire result)
more than 88'h00 = 0-1 for negative, 0 for positive

Code Examples — From Basics to Production

Example 1 — Beginner: All Four Operators

Example 1 — Shift Operator Basics
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_shift_basic;
 
  logic        [7:0] u_val  = 8'd16;   // unsigned 16 = 0001_0000
  logic signed [7:0] s_neg  = -8;      // signed  -8  = 1111_1000
  logic signed [7:0] s_pos  = 8'd16;   // signed +16  = 0001_0000
 
  initial begin
 
    // ── Left shifts — zero fills right, identical for << and <<< ──
    $display("u_val <<  2 = %0d  (16×4=64)",  u_val <<  2);  // 64
    $display("u_val <<< 2 = %0d  (same)",      u_val <<< 2);  // 64
 
    // ── Logical right — zero fills left ───────────────────────────
    $display("u_val >>  2 = %0d  (16÷4=4)",   u_val >>  2);   // 4
    $display("s_neg >>  2 = %0d  (WRONG!)",   s_neg >>  2);   // +62
 
    // ── Arithmetic right — sign fills left ────────────────────────
    $display("s_neg >>> 2 = %0d  (-8÷4=-2)",  s_neg >>> 2);  // -2
    $display("s_pos >>> 2 = %0d  (16÷4=4)",   s_pos >>> 2);  // 4 (positive: 0 fills)
    $display("u_val >>> 2 = %0d  (unsigned: 0 fill)", u_val >>> 2); // 4
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
u_val <<  2 = 64  (16×4=64)
u_val <<< 2 = 64  (same)
u_val >>  2 = 4   (16÷4=4)
s_neg >>  2 = 62  (WRONG!)
s_neg >>> 2 = -2  (-8÷4=-2)
s_pos >>> 2 = 4   (16÷4=4)
u_val >>> 2 = 4   (unsigned: 0 fill)

Example 2 — Intermediate: Address Calculation and Mask Generation

Example 2 — Shift for Address and Mask Operations
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_shift_address;
 
  logic [31:0] base_addr = 32'h1000_0000;
  logic [31:0] word_addr, byte_mask, field_mask;
  int          index;
 
  initial begin
 
    // ── Word-aligned address from byte index ──────────────────────
    // AXI/APB: word addresses are byte_addr >> 2
    // Byte address from word index: word_idx << 2
    for (int i = 0; i < 4; i++) begin
      word_addr = base_addr + (32'(i) << 2);
      $display("Word[%0d] addr = 0x%08h", i, word_addr);
    end
    // 0x10000000, 0x10000004, 0x10000008, 0x1000000C
 
    // ── Generate single-bit mask at position n ────────────────────
    for (int bit_pos = 0; bit_pos < 4; bit_pos++) begin
      byte_mask = 32'h1 << bit_pos;
      $display("Bit mask [%0d] = 0x%08h (%032b)",
               bit_pos, byte_mask, byte_mask);
    end
 
    // ── Extract byte N from 32-bit word ──────────────────────────
    logic [31:0] data = 32'hAABBCCDD;
    for (int byte_n = 0; byte_n < 4; byte_n++) begin
      logic [7:0] extracted = (data >> (byte_n * 8)) & 8'hFF;
      $display("Byte[%0d] = 0x%02h", byte_n, extracted);
    end
    // Byte[0]=0xDD, Byte[1]=0xCC, Byte[2]=0xBB, Byte[3]=0xAA
 
    $finish;
  end
 
endmodule

Example 3 — Verification-Oriented: Fixed-Point Arithmetic Checker

Example 3 — Signed Arithmetic Shift in Scoreboard
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// DSP core verification: right-shift with rounding
// DUT computes: (A × B) >>> SCALE, with half-up rounding
class DspScoreboard;
 
  localparam int SCALE = 8;   // right-shift amount
 
  function int expected_result(input int a, b);
    longint product = longint'(a) * longint'(b);
    longint half    = 1 << (SCALE - 1);       // 0.5 in fixed-point
    longint rounded = (product + half) >>> SCALE;  // arithmetic shift
    return int'(rounded);
  endfunction
 
  function void check(input int a, b, dut_out);
    int exp = expected_result(a, b);
    if (exp !== dut_out)
      $error("DSP FAIL: %0d * %0d >> %0d = exp %0d, got %0d",
             a, b, SCALE, exp, dut_out);
    else
      $display("DSP OK:   %0d * %0d >> %0d = %0d", a, b, SCALE, exp);
  endfunction
 
endclass
 
module tb_dsp;
  DspScoreboard sb;
  initial begin
    sb = new();
    sb.check(100,  200, 78);     // 100×200=20000, 20000>>8 ≈ 78
    sb.check(-100, 200, -78);    // -100×200=-20000, -20000>>8 ≈ -78
    $finish;
  end
endmodule

Example 4 — Corner Case: >>> on Unsigned Type — No Sign Extension

Example 4 — >>> on Unsigned Type: Zero-Fill, Not Sign-Fill
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_unsigned_arith_shift;
 
  logic        [7:0] u_val = 8'hF8;    // unsigned — MSB = 1, but not signed
  logic signed [7:0] s_val = 8'hF8;    // signed   — same bits = -8
 
  initial begin
    $display("u_val = %0d (unsigned 248)", u_val);
    $display("s_val = %0d (signed -8)",   s_val);
 
    // >>> on UNSIGNED — no sign extension, fills 0
    $display("u_val >>> 2 = %0d  (zero-fill: 248/4=62)", u_val >>> 2);
 
    // >>> on SIGNED — sign extension, fills 1s
    $display("s_val >>> 2 = %0d  (sign-fill: -8/4=-2)", s_val >>> 2);
 
    // If you WANT signed behavior on an unsigned variable — cast first
    $display("$signed(u_val) >>> 2 = %0d", $signed(u_val) >>> 2);
    // Now sign-extends: -8/4 = -2
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
u_val = 248 (unsigned 248)
s_val = -8 (signed -8)
u_val >>> 2 = 62  (zero-fill: 248/4=62)
s_val >>> 2 = -2  (sign-fill: -8/4=-2)
$signed(u_val) >>> 2 = -2

Waveform & Simulation Thinking

Result Width — Does Shifting Change the Width?

A shift's result width is set by context, not by the left operand alone. This is the most consequential rule on the page, and stating it as "the result is the same width as the value being shifted" — the common shorthand — is wrong in a way that produces real bugs.

The precise rule: in a shift expression the left operand is context-determined and the right operand (the shift amount) is self-determined. So the left operand is first extended to the width of the surrounding expression, and then shifted. If the shift stands alone — inside a $display argument, or compared against something narrow — its width is its own, and bits shift off the edge and are lost. There is no automatic widening. This is important when left-shifting a value that might overflow.

Result Width Is Set by Context, Not by the Operand
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [7:0]  a = 8'hFF;
logic [15:0] wide_result;
logic [7:0]  narrow_result;
 
// 8-bit context: a stays 8 bits, the MSB shifts out and is lost.
narrow_result = a << 1;          // 8'hFE
 
// 16-bit context: a is EXTENDED TO 16 BITS FIRST, then shifted.
// No concatenation and no cast is needed - the assignment target
// already widened the expression.
wide_result   = a << 1;          // 16'h01FE, not 16'h00FE
 
// These are the same value, written more explicitly. Use them when the
// intent should be visible without knowing the sizing rules - but know
// that they are documentation, not a fix for a truncation that would
// otherwise happen here.
wide_result   = {8'h00, a} << 1; // 16'h01FE
wide_result   = 16'(a)    << 1;  // 16'h01FE
 
// Self-determined context: no assignment target to widen it, so the
// shift is evaluated at 8 bits and the top bit is lost.
$display("in a display arg: 0x%0h", a << 1);   // 0xfe  <-- differs!
$display("narrow_result:    0x%0h", narrow_result); // 0xfe
$display("wide_result:      0x%0h", wide_result);   // 0x1fe

Look at the last three lines. a << 1 appears twice, character for character, and evaluates to 8'hFE in one place and 16'h01FE in the other. Nothing about the expression decides that — the surrounding context does.

This is the mechanism behind a whole family of reports that read as "the model and the DUT agree in simulation but disagree in the log", or "it works until I widen the accumulator." It is also why the signed case in the next section is dangerous: extension of a signed operand into a wider context happens before the shift, so s >> 1 in a 32-bit context shifts a sign-extended value and produces a large positive number rather than a small one.

Synthesis — Constant vs Variable Shift

Shift typeHardware generatedArea / Timing impact
Constant shift (val << 3)Pure wiring — no gates at allZero area, zero delay — just rewire the bits
Variable shift (val << n)A select network — conventionally a barrel shifter of ~log2(W) mux stages, though the tool choosesReal area and delay: cost ≈ data width × number of select stages
Arithmetic right (>>>) constantWiring + sign replicationNear-zero area — replicate MSB N times
Arithmetic right variableThe same select network, with the fill driven by the sign bit rather than tied to groundSlightly larger than the logical form — the fill comes from a signal, not a constant

Where You'll Use These in Real Projects

Real Verification Usage Patterns
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 1. Single-bit mask generation — error injection ───────────────
function logic [31:0] bit_mask(input int pos);
  return 32'h1 << pos;
endfunction
 
// ── 2. Protocol field packing — build from named fields ───────────
function logic [31:0] pack_axi_ctrl(
  input logic [1:0]  burst_type,
  input logic [2:0]  burst_len,
  input logic         write
);
  return ({29'h0, burst_type} << 16)
       | ({29'h0, burst_len}  << 8)
       | {31'h0, write};
endfunction
 
// ── 3. Byte lane enable from address ──────────────────────────────
function logic [3:0] byte_enables(input logic [31:0] addr, input int size);
  logic [3:0] mask = (4'h1 << size) - 4'h1;   // size bytes → N-bit mask
  return mask << addr[1:0];                      // align to byte offset
endfunction
 
// ── 4. Scoreboard: extract field from DUT response ────────────────
function logic [7:0] extract_status(input logic [31:0] reg_val);
  return (reg_val >> 16) & 8'hFF;   // bits [23:16]
endfunction
 
// ── 5. Constraint: aligned address generation ─────────────────────
class AlignedTxn;
  rand logic [31:0] addr;
  rand logic [1:0]  size;   // 0=byte, 1=halfword, 2=word
 
  constraint c_align {
    addr[1:0] == 2'b00;            // word-aligned
    addr inside {[32'h1000:32'h1FFF]};
  }
endclass

Common Bugs & How to Debug Them

Bug 1 — Logical >> on Signed Value: Wrong Arithmetic Result

Bug 1 — Using >> Instead of >>> on Signed Value
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// DUT computes signed accumulator / 4
// Scoreboard model uses wrong shift
int accumulator = -100;
 
// BUGGY: logical right shift fills 0s — makes negative → large positive
int result_wrong = accumulator >>  2;
$display("Wrong: %0d", result_wrong);   // 1073741799 — completely wrong
 
// CORRECT: arithmetic right shift preserves sign
int result_right = accumulator >>> 2;
$display("Right: %0d", result_right);   // -25 — correct (-100/4)

Bug 2 — Left Shift Overflow: Result Truncated to Operand Width

Bug 2 — Overflow in Left Shift: Bits Lost
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [7:0] val = 8'hC0;   // 1100_0000
 
// BUGGY: expecting 0x300 but result truncated to 8 bits
logic [7:0] result = val << 2;
$display("result = 0x%0h", result);   // 0x00 — top bits lost!
 
// CORRECT: widen before shifting
logic [9:0] result_wide = {2'b00, val} << 2;
$display("result_wide = 0x%0h", result_wide);   // 0x300 — correct

Bug 3 — >>> on logic [N:0] — No Sign Extension Happens

Bug 3 — Expecting >>> to Sign-Extend on Unsigned logic
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [7:0] data = 8'hF8;   // MSB=1 but type is UNSIGNED
 
// BUGGY: engineer expects sign extension because "MSB is 1"
// But logic [7:0] is unsigned — >>> fills 0s, not the MSB
logic [7:0] wrong = data >>> 2;
$display("Wrong: %0d", wrong);   // 62 — zero filled, not sign extended
 
// CORRECT option 1: declare as signed
logic signed [7:0] s_data = 8'hF8;   // same bits = -8
logic [7:0] correct1 = s_data >>> 2;
$display("Correct1: %0d", correct1);   // -2
 
// CORRECT option 2: cast at the shift expression
logic [7:0] correct2 = $signed(data) >>> 2;
$display("Correct2: %0d", correct2);   // -2

Proving It — Signedness, Context, and the Cast

Three rules decide every shift result: whether the left operand is signed, how wide the surrounding context is, and which operator you used. This testbench exercises all three against each other and self-checks, so a wrong belief fails here rather than in a scoreboard.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// shift_semantics_proof.sv
//
// Self-checking proof of >> vs >>>, signed vs unsigned operands, and the
// effect of the surrounding expression width. Every claim in this chapter
// has a corresponding check below.
module shift_semantics_proof;
 
  int errors = 0;
 
  task automatic chk(string name, logic [31:0] got, logic [31:0] exp);
    if (got !== exp) begin
      errors++;
      $display("FAIL %-46s got=%08h exp=%08h", name, got, exp);
    end else
      $display("pass %-46s = %08h", name, got);
  endtask
 
  initial begin
    logic signed [7:0] s;
    logic        [7:0] u;
    logic signed [7:0] r8s;
    logic        [7:0] r8u;
    int                r32;
 
    // ================================================================
    // 1. SAME BITS, DIFFERENT DECLARED SIGNEDNESS
    //    u and s hold the identical bit pattern 8'b1111_1000.
    // ================================================================
    s = -8;          // 8'b1111_1000
    u = 8'hF8;       // 8'b1111_1000  - the same bits
 
    r8s = s >>> 2;   chk("signed   s >>> 2  (sign fill)",  r8s, 8'hFE);  // -2
    r8u = u >>> 2;   chk("unsigned u >>> 2  (ZERO fill)",  r8u, 8'h3E);  // +62
 
    // The operator did not change. The DECLARATION did. This is the whole
    // point: >>> is not "the sign-extending shift", it is the shift that
    // respects signedness - and it has none to respect on `u`.
 
    // 2. The cast restores it, for this expression only.
    r8u = $signed(u) >>> 2;
    chk("$signed(u) >>> 2  (cast to signed)",  r8u, 8'hFE);
 
    // 3. >> ignores signedness entirely - both give the zero-filled result.
    r8s = s >> 2;    chk("signed   s >> 2   (still zero fill)", r8s, 8'h3E);
    r8u = u >> 2;    chk("unsigned u >> 2",                     r8u, 8'h3E);
 
    // ================================================================
    // 4. CONTEXT WIDTH - the same expression, two answers
    // ================================================================
    r8s = s >> 1;    chk("s >> 1 into 8-bit  target",  r8s, 8'h7C);       // +124
    r32 = s >> 1;    chk("s >> 1 into 32-bit target",  r32, 32'h7FFF_FFFC);
 
    // Why: assigning to `int` makes the shift a 32-bit expression, so the
    // context-determined left operand is SIGN-extended to 32'hFFFF_FFF8
    // FIRST, and only then shifted right with a zero fill.
    //
    // The arithmetic shift is stable across contexts, because sign-extending
    // and then sign-filling gives the same value at any width:
    r8s = s >>> 1;   chk("s >>> 1 into 8-bit  target",  r8s, 8'hFC);      // -4
    r32 = s >>> 1;   chk("s >>> 1 into 32-bit target",  r32, 32'hFFFF_FFFC); // -4
 
    // 5. Left shifts: <<< is identical to <<, for signed and unsigned alike.
    chk("s << 1  equals  s <<< 1", 8'(s << 1), 8'(s <<< 1));
    chk("u << 1  equals  u <<< 1", 8'(u << 1), 8'(u <<< 1));
 
    // 6. Over-shift. Logical goes to zero; arithmetic goes to the sign.
    chk("u >> 8   (over-shift, logical)",        8'(u >> 8),   8'h00);
    chk("s >>> 8  (over-shift, negative)",       8'(s >>> 8),  8'hFF);
    s = 8'sd8;
    chk("s >>> 8  (over-shift, positive)",       8'(s >>> 8),  8'h00);
 
    // 7. Floor division, not truncation. This is the reference-model trap.
    s = -7;
    chk("-7 >>> 2  is -2 (floor)",   8'(s >>> 2), 8'hFE);
    chk("-7 / 4    is -1 (truncate)", 8'(s / 4),  8'hFF);
    // They agree only when the division is exact:
    s = -8;
    chk("-8 >>> 2  ==  -8 / 4",      8'(s >>> 2), 8'(s / 4));
 
    if (errors == 0) $display("\nshift_semantics_proof: ALL CHECKS PASSED");
    else             $display("\nshift_semantics_proof: %0d FAILURES", errors);
    $finish;
  end
endmodule

Check 1 is the one to sit with. s and u hold the same eight bits. The same operator applied to them produces -2 and +62. Nothing in the expression distinguishes the two cases — only the declaration twenty lines away does.

A right shift result is determined by three inputs: the operator chooses whether signedness matters, the declaration supplies the signedness, and the context width sets the operand extension before shifting. A signed cast overrides the declaration for one expression.Operator: >> or >>>>> never consults signDeclarationsignednesslogic [7:0] vs signedContext widththe assignment target$signed / $unsignedoverrides, this expr onlyOperand extendedfirstsign- or zero-extendFill = sign bitsigned operand, >>>Fill = zero>> always; >>> if unsignedResult at contextwidthnot the operand width12
Figure 1 - what decides a right-shift result. The operator chooses whether signedness is consulted at all: >> always fills zero, >>> consults the operand. The declaration decides what >>> finds - a signed operand fills with the sign bit, an unsigned one fills with zero, so >>> on an unsigned value behaves exactly like >>. The surrounding expression width decides how wide the operand is extended before any of this happens. A $signed cast overrides the declaration for one expression only.
1

A signed audio sample was declared unsigned, so the arithmetic shift filled zeros

UNSIGNED-FIELD-BREAKS-SIGN-FILL
Buggy Code & Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: the sample field is 16-bit two's-complement audio, but the struct
//         declares it unsigned. The gain stage uses >>> deliberately, to
//         preserve the sign - and it does not, because there is no sign to
//         preserve at the language level.
typedef struct packed {
  logic [7:0]  channel;
  logic [15:0] sample;      // ❌ two's-complement data, declared UNSIGNED
  logic [7:0]  flags;
} audio_word_t;
 
always_comb begin
  audio_word_t w = unpack(bus_data);
  scaled = w.sample >>> gain_shift;   // intent: divide, keep the sign
end                                   // actual: zero fill, positive result
 
// For w.sample = 16'hFF00 (-256) and gain_shift = 2:
//   intended   -256 >>> 2  = -64      = 16'hFFC0
//   actual   16'hFF00 >>> 2 = 16'h3FC0 = +16320
 
// ✅ FIX (preferred): declare the field with the signedness the data has.
//    This fixes every operation on it - shift, compare, extend, divide -
//    not just the one expression that happened to be noticed.
typedef struct packed {
  logic        [7:0]  channel;
  logic signed [15:0] sample;    // ✅ signed, as the data actually is
  logic        [7:0]  flags;
} audio_word_t;
 
// ✅ FIX (when the struct is not yours to change): cast at the point of use.
scaled = $signed(w.sample) >>> gain_shift;
Symptom

A digital gain stage passed every directed test and failed in the lab as a distinctive, reproducible distortion: quiet passages were clean, loud passages buzzed, and an oscilloscope on the output showed the negative half of the waveform folded up into the positive rail.

The RTL used >>>. The reviewer had specifically asked for >>> rather than >> during code review, and had confirmed it was there.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
gain_shift = 2
  input sample  16'hFF00   (-256)
  expected      16'hFFC0   (-64)
  observed      16'h3FC0   (+16320)
Root Cause

>>> is not "the sign-extending shift." It is the shift that respects the signedness of its left operand, and logic [15:0] is unsigned. Presented with an unsigned operand, >>> fills zeros — which is correct, specified behaviour, and identical to >>.

So the operator that was chosen specifically to protect the sign did nothing, and the code review that confirmed its presence confirmed the wrong thing. The reviewer checked the operator; the bug was in the declaration, in a different file.

Two properties made this survive testing.

The directed tests used positive samples. For any non-negative value >> and >>> produce identical results, so the entire distinction is invisible until a negative number appears. A test suite built from ascending ramps and tone bursts starting at zero can exercise the gain stage thousands of times without ever discriminating between the two operators.

The wrong answer is in range. +16320 is a perfectly legal 16-bit sample. There is no X, no overflow flag, no assertion to trip. The only detector is a model that knows the value should have been negative — and the reference model had inherited the same unsigned struct definition, so it computed the same wrong answer and the scoreboard agreed with the DUT.

That last point is the general lesson. A reference model that shares a type definition with the DUT cannot catch a bug in that type definition. The struct was the single source of truth for both, so the error was outside what the comparison could see.

Fix

Fix the declaration, not the expression. logic signed [15:0] sample corrects the shift and also every comparison, extension, and division that touches the field — including ones not yet written. The cast form $signed(w.sample) >>> gain_shift is correct but repairs exactly one expression and leaves the next author to rediscover the problem.

Three guards, in increasing order of what they catch:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 1. Make the intent checkable at the boundary. A signed field that has
//    just been shifted right arithmetically cannot have changed sign.
a_gain_preserves_sign: assert property (@(posedge clk) disable iff (!rst_n)
  (gain_shift != 0) |-> ($signed(scaled) <= 0) == ($signed(w.sample) <= 0))
  else $error("gain stage changed the sign of a sample");
 
// 2. Cover the case the directed tests never produced. A `negative` bin
//    at zero hits is the signature of a suite that could not have found
//    this, whatever else it covered.
covergroup cg_sample;
  cp_sign : coverpoint $signed(w.sample) {
    bins negative = {[-32768:-1]};
    bins zero     = {0};
    bins positive = {[1:32767]};
  }
endgroup
 
// 3. Lint. Most tools can flag >>> applied to an operand that is not
//    signed. It is a warning by default in several; make it an error.

The habits worth taking from this beyond the immediate fix:

  • Declare signedness where the data is defined, not where it is used. A cast fixes one expression; a declaration fixes the type. If a field holds two's-complement data, say so in the struct.
  • >>> on an unsigned operand is a code smell, not a safeguard. It reads as protective and is inert. Either the operand should be signed, or >> is the honest operator.
  • A test suite that never drives a negative value cannot distinguish >> from >>>. Cover the sign of every signed field. It is a one-line coverpoint and it is the only guard here that works when the reviewer, the model, and the lint rule have all been defeated by a shared type definition.

Interview Questions

Best Practices & Coding Guidelines

Declare signed types explicitly

For signed data, use int, shortint, or logic signed [N:0]. Never rely on a high MSB alone — the type determines >>> behavior.

Widen before shifting left

If the left shift result needs to be wider than the operand, widen first: 16'(narrow) << n or concatenate zeros. Shifting a narrow variable silently discards high bits.

Use >>> only on genuinely signed operands

Use >>> exclusively for signed division-by-power-of-two. For unsigned operations, >> is clearer and communicates intent.

Match model to DUT implementation

If the DUT uses arithmetic shift, your scoreboard reference model must also use arithmetic shift — not integer division, which truncates differently for negative non-exact values.

TaskCorrect approachCommon mistake
Multiply unsigned by 2^nval << nForgetting result is same width — overflow silently
Divide unsigned by 2^nval >> nUsing >>> — works but wrong operator semantically
Divide signed by 2^nsigned_val >>> nUsing >> — wrong result for negative values
Generate bit mask at position n32'h1 << n1 << n — 1 is 32-bit int but expression context may truncate
Extract upper byte from 32-bit(val >> 24) & 8'hFFForgetting the AND mask — implicit sign-extension can corrupt
Left shift wider result needed16'(val) << nval << n — result stays at original width

Summary

Four shift operators, but the real decision you make is between two: >> and >>>. Left shifts (<< and <<<) are identical. Right shifts diverge only for signed negative values.

  • >>> only sign-extends when the operand is a signed type. Applying >>> to logic [N:0] produces the same zero-fill result as >>. The operator does not override the declaration. Use $signed() to cast if needed.
  • Arithmetic right shift is floor division, not truncation. For negative non-exact values, x >>> n gives a different result than x / (1 << n). Your scoreboard reference model must match the DUT's implementation exactly.
  • Left shifts do not widen the result. The result is always the same width as the left operand. Bits shifted past the MSB are lost. Widen before shifting when the full result is needed.

Signedness is the other half of this topic. A shift operator can only respect the signedness its operand was declared with, so Integer Types and 2-state vs 4-state Types decide what >>> will do before the shift is ever written. Arithmetic Operators covers the division that a shift is often standing in for — and the truncate-toward-zero behaviour that makes / 4 differ from >>> 2 on negative operands.

Where shifts sit among the other operators. Shift binds below arithmetic, so a + 2 << 3 is (a + 2) << 3 — see Operator Precedence, which also covers why parentheses do not contain a width. For the per-bit operations shifts are often combined with, see Bitwise Operators; for building a shifted field into a wider word, Concatenation, Replication & Conditional, whose braces are a sizing boundary in the way parentheses are not. For the loops that a variable shift is sometimes written as by mistake, see Loops. For the unary &, |, ^ that collapse a shifted vector to a status bit, see Reduction Operators; for the truth-value forms used in the conditions that guard a shift, Logical Operators.

References.

  • IEEE 1800 (SystemVerilog) — the shift operators are defined in the operators-and-expressions clause. Two rules on this page trace directly to it and were corrected against it: >>> fills with the sign bit only when the left operand is signed, filling zero otherwise; and in a shift expression the left operand is context-determined while the shift amount is self-determined, which is why the same expression yields a different value depending on the width of what it is assigned to. The signedness rules for expressions, and the $signed/$unsigned system functions, are in the same clause.
  • IEEE 1364 (Verilog)<<< and >>> were introduced in Verilog-2001 along with signed types; the semantics are unchanged in SystemVerilog.

Requirement versus practice. The fill behaviour, the context-determination rule, and the over-shift results are language requirements — a conforming simulator has no latitude on any of them. What a synthesis tool builds for a variable shift is not specified: a barrel shifter of roughly log₂(W) mux stages is the conventional structure and a reasonable cost model, but the topology depends on the library, the constraints, and the surrounding logic. This page gives the cost model and avoids asserting the structure.

Part of SystemVerilog Fundamentals·Operators & Expressions·Lesson 24 of 53

View program

Continue learning