Skip to content

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: -8 >> 1 gives +120 on an 8-bit signed variable (the MSB shifts in as 0, the negative number becomes large positive). -8 >>> 1 gives -4, which is the correct arithmetic result. Both operations succeed silently. No error, no warning — just a wrong number feeding the rest of your datapath.

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 — Shifts bits toward MSB, fills 0s on the right. Equivalent to multiplying by 2. Bits shifted out of the MSB are lost. Works identically to <<<.
  • >> — Logical Right — 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 — 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 — Fills 0s on the right — identical to << in all cases. Exists for language symmetry. Safe to use either form for left shifts.

Syntax & Type Rules

SystemVerilog — Shift Operator Syntax
// 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)
> 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
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
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
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
// 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
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
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?

In SystemVerilog, shift operators do not change the width of the result. The result is the same width as the left operand (the value being shifted). Bits that shift off the edge are simply lost — there is no automatic widening. This is important when left-shifting a value that might overflow.

Result Width and Overflow Behavior
logic [7:0]  a = 8'hFF;
logic [15:0] wide_result;
logic [7:0]  narrow_result;
 
// Result is same width as LHS — truncated to 8 bits
narrow_result = a << 1;     // 8'h1FE & 8'hFF mask = 8'hFE — MSB lost
 
// Widen BEFORE shifting to preserve all bits
wide_result = {8'h00, a} << 1;   // concat to 16 bits first: 16'h01FE
// Or equivalently:
wide_result = 16'(a) << 1;        // explicit width cast
 
$display("narrow: 0x%0h", narrow_result);  // 0xfe
$display("wide:   0x%0h", wide_result);    // 0x1fe

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)Barrel shifter — multiplexer treeSignificant area and timing — scales with log2(width) MUX levels
Arithmetic right (>>>) constantWiring + sign replicationNear-zero area — replicate MSB N times
Arithmetic right variableBarrel shifter with sign-aware MUXSlightly larger than logical barrel shifter

Where You'll Use These in Real Projects

Real Verification Usage Patterns
// ── 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
// 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
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
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

Interview Questions

Beginner Level

Q1: What is the difference between >> and >>> in SystemVerilog?>> is logical right shift — always fills vacated MSB positions with 0. >>> is arithmetic right shift — fills with the sign bit for signed types (preserving the sign), and with 0 for unsigned types. For positive values and unsigned types, they produce the same result. The difference only appears with signed negative values. Q2: What does <<< do differently from <<? Nothing. Both logical and arithmetic left shift always fill vacated right-side (LSB) positions with 0, regardless of the operand's sign. <<< exists for symmetry with the four-operator set but produces identical results to <<. Either form is correct; teams typically standardize on one or the other in their coding guidelines.

Intermediate Level

Q3: A signed 8-bit variable holds -8 (8'b1111_1000). What is the result of >> 2 vs >>> 2?>> 2 (logical): fills 0s from left → 8'b0011_1110 = +62. Wrong for signed arithmetic.>>> 2 (arithmetic): fills sign bit (1) from left → 8'b1111_1110 = -2. Correct — -8 divided by 4 is -2. Q4: What happens when the shift amount equals or exceeds the operand width? For logical shifts (<<, >>) and arithmetic left: the result is 0 — all bits shift out. For arithmetic right shift (>>>) on a signed type: the result is all copies of the sign bit — all 1s for negative, all 0s for positive. The shift amount is always treated as unsigned, so a negative shift amount would be treated as a very large positive shift, producing 0 or all-sign-bits.

Experienced Engineer Level

Q5: A scoreboard models a DUT that performs signed division by powers of two. The DUT uses arithmetic right shift but the model uses integer division (/). For -7 ÷ 4, do they produce the same result? No. -7 >>> 2 = -2 (floor division: ⌊-7/4⌋ = ⌊-1.75⌋ = -2). -7 / 4 = -1 (truncation toward zero: -1.75 truncates to -1). Arithmetic right shift performs floor division, not truncation-toward-zero. They agree for exact powers of two (e.g., -8/4=-2), but diverge for non-exact negative values. A scoreboard that uses / to model a DUT that uses >>> will report false mismatches on these edge cases. Always model the DUT's actual shift behavior in the reference model.

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 for signed arithmetic — 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.