Skip to main content
This page is a placeholder. All examples on this page are currently AI-generated and are not correct. This documentation will be completed in the future with accurate, tested examples.

Overview

Opcode: 0x1d Introduced: Constantinople (EIP-145) SAR performs arithmetic (signed) shift right on a 256-bit value interpreted as a two’s complement signed integer. Vacated bits (on the left) are filled with the sign bit (MSB), preserving the sign of the value. This operation efficiently divides signed integers by powers of 2 with correct rounding toward negative infinity. Before EIP-145, signed right shifts required expensive SDIV + EXP operations. SAR reduces this to 3 gas. Note: SAR is for signed values. For unsigned division, use SHR (0x1c).

Specification

Stack Input:
shift (top) - number of bit positions to shift
value - 256-bit value (interpreted as signed i256)
Stack Output:
value >> shift (arithmetic, sign-fill)
Gas Cost: 3 (GasFastestStep) Hardfork: Constantinople (EIP-145) or later Shift Behavior:
shift < 256:
  - Positive: value >> shift (zero-fill, same as SHR)
  - Negative: value >> shift (one-fill, preserves sign)
shift >= 256:
  - Positive: 0
  - Negative: 0xFFFF...FFFF (-1)

Behavior

SAR pops two values from the stack:
  1. shift - number of bit positions to shift right (0-255)
  2. value - 256-bit value interpreted as signed i256 (two’s complement)
Result is value shifted right by shift positions, with the sign bit (MSB) replicated to fill vacated high-order bits. Sign Extension:
  • If MSB = 0 (positive): fill with 0s (same as SHR)
  • If MSB = 1 (negative): fill with 1s (preserve negative sign)
Overflow behavior:
  • shift >= 256 and positive: result = 0
  • shift >= 256 and negative: result = -1 (0xFFFF…FFFF)

Examples

Positive Value (Same as SHR)

import { sar } from '@tevm/voltaire/evm/bitwise';
import { createFrame } from '@tevm/voltaire/evm/Frame';

// Positive value: SAR behaves like SHR
const value = 0x1000n;  // MSB = 0 (positive)
const frame = createFrame({ stack: [4n, value] });
const err = sar(frame);

console.log(frame.stack[0].toString(16));  // '100' (zero-filled)

Negative Value (Sign Extension)

// Negative value: SAR preserves sign
const negativeValue = (1n << 255n) | 0xFFn;  // MSB = 1 (negative)
const frame = createFrame({ stack: [4n, negativeValue] });
sar(frame);

// High bits filled with 1s (sign-extended)
const expectedMsb = 1n << 251n;  // MSB shifted to bit 251
console.log((frame.stack[0] >> 251n) & 1n);  // 1 (sign preserved)

Divide Negative by Power of 2

// SAR correctly divides signed integers
// -16 / 4 = -4
const minussixteen = (1n << 256n) - 16n;  // Two's complement of -16
const frame = createFrame({ stack: [2n, minussixteen] });
sar(frame);

const minusFour = (1n << 256n) - 4n;
console.log(frame.stack[0] === minusFour);  // true (-4 in two's complement)

Maximum Shift on Negative

// Shift >= 256 on negative → -1 (all ones)
const negativeValue = 1n << 255n;  // -2^255 in two's complement
const frame = createFrame({ stack: [256n, negativeValue] });
sar(frame);

const allOnes = (1n << 256n) - 1n;
console.log(frame.stack[0] === allOnes);  // true (-1)

Maximum Shift on Positive

// Shift >= 256 on positive → 0
const positiveValue = (1n << 254n);  // Large positive
const frame = createFrame({ stack: [256n, positiveValue] });
sar(frame);

console.log(frame.stack[0]);  // 0n

SAR vs SHR Comparison

// Same negative value, different shifts
const negValue = 1n << 255n;  // MSB set

// SHR: logical (zero-fill)
const frameSHR = createFrame({ stack: [1n, negValue] });
shr(frameSHR);
console.log(frameSHR.stack[0] === (1n << 254n));  // true (bit 254, positive)

// SAR: arithmetic (sign-fill)
const frameSAR = createFrame({ stack: [1n, negValue] });
sar(frameSAR);
const expectedSAR = (1n << 255n) | (1n << 254n);  // Both bits 254 and 255 set
console.log(frameSAR.stack[0] === expectedSAR);  // true (still negative)

Gas Cost

Cost: 3 gas (GasFastestStep) Pre-EIP-145 equivalent:
// Before Constantinople: expensive!
result = int256(value) / int256(2 ** shift)  // SDIV (5 gas) + EXP (10 + 50/byte gas)
// Total: 15-1615 gas

// After Constantinople: cheap!
assembly { result := sar(shift, value) }  // 3 gas
Savings: 12-1612 gas per signed shift operation.

Edge Cases

Zero Value

// Shifting zero always yields zero (sign = 0)
const frame = createFrame({ stack: [100n, 0n] });
sar(frame);

console.log(frame.stack[0]);  // 0n

Zero Shift

// No shift = identity
const value = 0xDEADBEEFn;
const frame = createFrame({ stack: [0n, value] });
sar(frame);

console.log(frame.stack[0] === value);  // true

Minus One

// -1 (all ones) shifted remains -1
const minusOne = (1n << 256n) - 1n;
const frame = createFrame({ stack: [100n, minusOne] });
sar(frame);

console.log(frame.stack[0] === minusOne);  // true (sign-extended)

Minimum Signed Int

// MIN_INT256 = -2^255
const MIN_INT = 1n << 255n;
const frame = createFrame({ stack: [1n, MIN_INT] });
sar(frame);

// -2^255 / 2 = -2^254 (sign-extended)
const expected = (1n << 255n) | (1n << 254n);
console.log(frame.stack[0] === expected);  // true

Maximum Positive Int

// MAX_INT256 = 2^255 - 1 (MSB = 0, all other bits = 1)
const MAX_INT = (1n << 255n) - 1n;
const frame = createFrame({ stack: [1n, MAX_INT] });
sar(frame);

// Result: zero-filled (positive)
const expected = (1n << 254n) - 1n;
console.log(frame.stack[0] === expected);  // true

Shift to Sign Bit Only

// Shift negative value to leave only sign bit
const negValue = (1n << 255n) | 0xFFFFn;
const frame = createFrame({ stack: [255n, negValue] });
sar(frame);

const allOnes = (1n << 256n) - 1n;
console.log(frame.stack[0] === allOnes);  // true (all 1s)

Edge of Sign Change

// Value with only bit 254 set (large positive)
const value = 1n << 254n;  // MSB = 0
const frame = createFrame({ stack: [1n, value] });
sar(frame);

// Result: 1 << 253 (still positive)
console.log(frame.stack[0] === (1n << 253n));  // true

Hardfork Check

// SAR is invalid before Constantinople
const frame = createFrame({
  stack: [4n, 0xFF00n],
  hardfork: 'byzantium'
});
const err = sar(frame);

console.log(err);  // { type: "InvalidOpcode" }

Stack Underflow

const frame = createFrame({ stack: [4n] });
const err = sar(frame);

console.log(err);  // { type: "StackUnderflow" }

Out of Gas

const frame = createFrame({ stack: [4n, 0xFF00n], gasRemaining: 2n });
const err = sar(frame);

console.log(err);  // { type: "OutOfGas" }

Common Usage

Signed Division by Power of 2

// Efficient signed division
function divideBy8(int256 value) pure returns (int256) {
    assembly {
        result := sar(3, value)  // Divide by 2^3 = 8
    }
    return result;
}

Extract Signed Value from Packed Data

// Extract signed 16-bit value from lower bits
function extractInt16(uint256 packed) pure returns (int16) {
    uint256 raw = packed & 0xFFFF;  // Extract 16 bits

    // Sign-extend to 256 bits
    int256 extended;
    assembly {
        // Shift left to MSB, then SAR back (sign-extends)
        extended := sar(240, shl(240, raw))
    }

    return int16(extended);
}

Fixed-Point Arithmetic

// Q64.64 fixed-point division by 2
function halfFixedPoint(int128 value) pure returns (int128) {
    // value is Q64.64: upper 64 bits integer, lower 64 fractional
    // Shift right 1 to divide by 2 (preserves sign)
    assembly {
        result := sar(1, value)
    }
    return int128(result);
}

Sign Extension

// Extend sign from bit position
function signExtend(uint256 value, uint256 bitPos) pure returns (uint256) {
    require(bitPos < 256, "bit position out of range");

    // Shift to align sign bit with MSB, then SAR back
    uint256 shift = 255 - bitPos;
    assembly {
        value := sar(shift, shl(shift, value))
    }
    return value;
}

Check Sign of Packed Int

// Check if packed signed value is negative
function isNegative(uint256 packed, uint256 bitWidth) pure returns (bool) {
    require(bitWidth > 0 && bitWidth <= 256, "invalid bit width");

    // Extract sign bit
    uint256 signBit = (packed >> (bitWidth - 1)) & 1;
    return signBit == 1;
}

Implementation

/**
 * SAR opcode (0x1d) - Arithmetic shift right operation (EIP-145)
 */
export function sar(frame: FrameType): EvmError | null {
  // Check hardfork (Constantinople or later)
  if (frame.hardfork.isBefore('constantinople')) {
    return { type: "InvalidOpcode" };
  }

  // Consume gas (GasFastestStep = 3)
  frame.gasRemaining -= 3n;
  if (frame.gasRemaining < 0n) {
    frame.gasRemaining = 0n;
    return { type: "OutOfGas" };
  }

  // Pop operands
  if (frame.stack.length < 2) return { type: "StackUnderflow" };
  const shift = frame.stack.pop();
  const value = frame.stack.pop();

  // Compute arithmetic shift right (sign-fill)
  // Convert to signed interpretation
  const isNegative = (value >> 255n) === 1n;

  let result: bigint;
  if (shift >= 256n) {
    // Overflow: return 0 or -1 based on sign
    result = isNegative ? (1n << 256n) - 1n : 0n;
  } else {
    // Arithmetic shift with sign extension
    result = value >> shift;

    // Sign-fill: if negative, fill upper bits with 1s
    if (isNegative && shift > 0n) {
      const fillBits = ((1n << shift) - 1n) << (256n - shift);
      result |= fillBits;
    }
  }

  // Push result
  if (frame.stack.length >= 1024) return { type: "StackOverflow" };
  frame.stack.push(result);

  // Increment PC
  frame.pc += 1;

  return null;
}

Testing

Test Coverage

import { describe, it, expect } from 'vitest';
import { sar } from './sar.js';

describe('SAR (0x1d)', () => {
  it('shifts positive value (same as SHR)', () => {
    const frame = createFrame({ stack: [4n, 0x1000n] });
    expect(sar(frame)).toBeNull();
    expect(frame.stack[0]).toBe(0x100n);
  });

  it('sign-extends negative value', () => {
    const negValue = 1n << 255n;  // MSB set
    const frame = createFrame({ stack: [1n, negValue] });
    expect(sar(frame)).toBeNull();

    // Result should have both bit 255 and 254 set (sign-extended)
    const expected = (1n << 255n) | (1n << 254n);
    expect(frame.stack[0]).toBe(expected);
  });

  it('divides negative by power of 2', () => {
    // -16 / 4 = -4
    const minus16 = (1n << 256n) - 16n;
    const frame = createFrame({ stack: [2n, minus16] });
    expect(sar(frame)).toBeNull();

    const minus4 = (1n << 256n) - 4n;
    expect(frame.stack[0]).toBe(minus4);
  });

  it('returns -1 for shift >= 256 on negative', () => {
    const negValue = 1n << 255n;
    const frame = createFrame({ stack: [256n, negValue] });
    expect(sar(frame)).toBeNull();

    const minusOne = (1n << 256n) - 1n;
    expect(frame.stack[0]).toBe(minusOne);
  });

  it('returns 0 for shift >= 256 on positive', () => {
    const posValue = 1n << 254n;
    const frame = createFrame({ stack: [256n, posValue] });
    expect(sar(frame)).toBeNull();
    expect(frame.stack[0]).toBe(0n);
  });

  it('handles zero shift (identity)', () => {
    const value = 0x123456n;
    const frame = createFrame({ stack: [0n, value] });
    expect(sar(frame)).toBeNull();
    expect(frame.stack[0]).toBe(value);
  });

  it('shifts -1 remains -1', () => {
    const minusOne = (1n << 256n) - 1n;
    const frame = createFrame({ stack: [100n, minusOne] });
    expect(sar(frame)).toBeNull();
    expect(frame.stack[0]).toBe(minusOne);
  });

  it('handles MIN_INT256', () => {
    const MIN_INT = 1n << 255n;
    const frame = createFrame({ stack: [1n, MIN_INT] });
    expect(sar(frame)).toBeNull();

    // -2^255 / 2 = -2^254 (sign-extended)
    const expected = (1n << 255n) | (1n << 254n);
    expect(frame.stack[0]).toBe(expected);
  });

  it('differs from SHR on negative values', () => {
    const negValue = 1n << 255n;

    // SHR: logical (zero-fill)
    const frameSHR = createFrame({ stack: [1n, negValue] });
    shr(frameSHR);

    // SAR: arithmetic (sign-fill)
    const frameSAR = createFrame({ stack: [1n, negValue] });
    sar(frameSAR);

    expect(frameSHR.stack[0]).not.toBe(frameSAR.stack[0]);
    expect(frameSHR.stack[0]).toBe(1n << 254n);  // Positive
    expect(frameSAR.stack[0]).toBe((1n << 255n) | (1n << 254n));  // Negative
  });

  it('returns InvalidOpcode before Constantinople', () => {
    const frame = createFrame({
      stack: [4n, 0xFF00n],
      hardfork: 'byzantium'
    });
    expect(sar(frame)).toEqual({ type: 'InvalidOpcode' });
  });

  it('returns StackUnderflow with insufficient stack', () => {
    const frame = createFrame({ stack: [4n] });
    expect(sar(frame)).toEqual({ type: 'StackUnderflow' });
  });

  it('returns OutOfGas when insufficient gas', () => {
    const frame = createFrame({ stack: [4n, 0xFF00n], gasRemaining: 2n });
    expect(sar(frame)).toEqual({ type: 'OutOfGas' });
  });
});

Edge Cases Tested

  • Positive value shifts (same as SHR)
  • Negative value sign extension
  • Signed division by powers of 2
  • Shift >= 256 on positive (→ 0)
  • Shift >= 256 on negative (→ -1)
  • Zero shift (identity)
  • -1 shifted remains -1
  • MIN_INT256 handling
  • MAX_INT256 handling
  • Comparison with SHR
  • Hardfork compatibility
  • Stack underflow
  • Out of gas

Security

SHR vs SAR Confusion

// CRITICAL: Using wrong shift for signed values
function divideSignedBy4(int256 value) pure returns (int256) {
    assembly {
        result := shr(2, value)  // WRONG! Treats as unsigned
    }
    return result;
}

// CORRECT: Use SAR for signed
function divideSignedBy4(int256 value) pure returns (int256) {
    assembly {
        result := sar(2, value)  // Preserves sign
    }
    return result;
}

// Example:
// value = -16 (0xFFFF...FFF0)
// SHR: 0x3FFF...FFFC (large positive, WRONG!)
// SAR: 0xFFFF...FFFC (-4, correct)

Rounding Direction

// SAR rounds toward negative infinity (floor division)
function divideSigned(int256 a, int256 b) pure returns (int256) {
    // Only use SAR if b is a power of 2
    require(isPowerOf2(uint256(b)), "not power of 2");

    uint256 shift = log2(uint256(b));
    assembly {
        result := sar(shift, a)
    }
    return result;
}

// Note: -7 / 4 using SAR = -2 (floor)
//       -7 / 4 in Solidity SDIV = -1 (truncate toward zero)

Sign Extension Pitfalls

// WRONG: Assuming SAR on unsigned creates signed
function makeNegative(uint256 value) pure returns (int256) {
    assembly {
        value := sar(1, value)  // Doesn't make value negative!
    }
    return int256(value);
}

// SAR interprets existing sign bit, doesn't change sign

Mixed Signed/Unsigned Operations

// DANGEROUS: Mixing signed and unsigned shifts
function process(uint256 value, bool isSigned) pure returns (uint256) {
    assembly {
        switch isSigned
        case 0 { value := shr(4, value) }  // Unsigned
        case 1 { value := sar(4, value) }  // Signed
    }
    return value;
}

// Risk: Type confusion if isSigned doesn't match value's actual signedness

Benchmarks

SAR is one of the fastest EVM operations: Execution time (relative):
  • SAR: 1.0x (baseline, fastest tier)
  • SHR/SHL: 1.0x (same tier)
  • SDIV: 2.5x
Gas comparison (signed right shift by 3):
MethodGasNotes
SAR (Constantinople+)3Native arithmetic shift
SDIV (pre-EIP-145)5value / 8
EXP + SDIV (variable)65+value / 2^shift
Gas savings: 2-1612 gas per signed shift vs pre-EIP-145 methods.

References