CrackedRuby logo

CrackedRuby

Bitwise Operators

Overview

Ruby implements bitwise operators as methods on Integer objects, providing direct manipulation of individual bits within numeric values. The language supports six primary bitwise operations: AND (&), OR (|), XOR (^), NOT (~), left shift (<<), and right shift (>>). These operators work at the binary level, treating integers as sequences of bits and applying logical or arithmetic operations to each bit position.

Ruby's bitwise operators inherit behavior from the underlying integer representation, working seamlessly with both positive and negative numbers through two's complement arithmetic. The operators return new Integer objects rather than modifying existing values, maintaining Ruby's immutable number semantics.

# Basic bitwise AND operation
binary_a = 0b1010  # 10 in decimal
binary_b = 0b1100  # 12 in decimal
result = binary_a & binary_b
# => 8 (0b1000)
# Shifting operations for power-of-two multiplication
base_value = 5
doubled = base_value << 1    # 10
quadrupled = base_value << 2 # 20
# XOR for toggle operations
flags = 0b11110000
toggle_mask = 0b00001111
toggled = flags ^ toggle_mask
# => 255 (0b11111111)

Bitwise operations find application in flag systems, cryptographic algorithms, performance optimization, bit manipulation puzzles, and low-level system programming. Ruby handles arbitrary precision integers, allowing bitwise operations on numbers of any size without overflow concerns that plague fixed-width integer systems.

The bitwise operators integrate with Ruby's numeric tower, automatically promoting results to appropriate types. Operations between different numeric types follow Ruby's coercion rules, with bitwise operators requiring integer operands or objects that respond to to_int.

Basic Usage

The AND operator (&) performs logical conjunction on corresponding bit pairs, returning 1 only when both input bits equal 1. This operation proves essential for masking operations, extracting specific bit patterns, and implementing flag checking systems.

# Extracting lower 4 bits using AND mask
number = 0b11010110
lower_nibble = number & 0b00001111
# => 6 (0b00000110)

# Checking if number is even (last bit is 0)
def even?(n)
  (n & 1) == 0
end

even?(42)  # => true
even?(17)  # => false

The OR operator (|) performs logical disjunction, setting result bits to 1 when either input bit equals 1. OR operations enable bit setting, flag combination, and value merging operations.

# Setting specific flag bits
permissions = 0b00000000
read_flag   = 0b00000100
write_flag  = 0b00000010
exec_flag   = 0b00000001

full_permissions = permissions | read_flag | write_flag | exec_flag
# => 7 (0b00000111)

# Combining option flags
color_red   = 0b001
color_green = 0b010
color_blue  = 0b100

purple = color_red | color_blue    # => 5 (0b101)
yellow = color_red | color_green   # => 3 (0b011)

The XOR operator (^) produces 1 when input bits differ and 0 when they match. XOR enables toggling operations, parity calculations, and simple encryption techniques.

# Toggle specific bits
status = 0b10101010
toggle_pattern = 0b11110000
new_status = status ^ toggle_pattern
# => 90 (0b01011010)

# Simple encryption/decryption with XOR
def xor_cipher(data, key)
  data.bytes.map.with_index { |byte, i| byte ^ key[i % key.length] }
end

message = "HELLO"
key = [0x5A, 0x3C, 0x7E]
encrypted = xor_cipher(message, key)
decrypted = xor_cipher(encrypted.pack('C*'), key)
# decrypted == "HELLO"

The NOT operator (~) inverts all bits, changing 0 to 1 and 1 to 0. Ruby implements NOT through two's complement arithmetic, producing negative values for positive inputs.

# Bit inversion
original = 0b00001111  # 15
inverted = ~original   # -16

# Two's complement demonstration
positive = 42
negative = ~positive + 1  # -42

Left shift (<<) moves bits leftward, filling vacant positions with zeros. Each left shift by one position multiplies the value by two, providing efficient power-of-two arithmetic.

# Power-of-two multiplication
base = 3
times_four = base << 2     # 12 (3 * 2^2)
times_eight = base << 3    # 24 (3 * 2^3)

# Building bit patterns
pattern = 1
pattern <<= 4  # 16 (0b00010000)
pattern <<= 2  # 64 (0b01000000)

Right shift (>>) moves bits rightward, performing arithmetic right shift that preserves sign for negative numbers. Right shifting by one position performs integer division by two, discarding fractional parts.

# Integer division by powers of two
dividend = 100
half = dividend >> 1      # 50
quarter = dividend >> 2   # 25

# Sign preservation with negative numbers
negative = -8
shifted = negative >> 1   # -4 (not 2147483644)

Advanced Usage

Bitwise operators combine to create sophisticated bit manipulation patterns. Bit field implementations use multiple operators together to pack and unpack data efficiently, storing several boolean or small integer values within single integers.

class BitField
  def initialize(value = 0)
    @value = value
  end
  
  def set_bit(position)
    @value |= (1 << position)
  end
  
  def clear_bit(position)
    @value &= ~(1 << position)
  end
  
  def toggle_bit(position)
    @value ^= (1 << position)
  end
  
  def bit_set?(position)
    (@value & (1 << position)) != 0
  end
  
  def extract_range(start, length)
    mask = (1 << length) - 1
    (@value >> start) & mask
  end
  
  def insert_value(value, start, length)
    mask = ((1 << length) - 1) << start
    @value = (@value & ~mask) | ((value << start) & mask)
  end
end

bf = BitField.new
bf.set_bit(3)        # Set bit 3
bf.set_bit(7)        # Set bit 7
bf.insert_value(5, 0, 3)  # Insert value 5 in bits 0-2
# bf.@value is now 0b10001101 (141)

Hash function implementations leverage bitwise operations for efficient key distribution and collision reduction. The XOR operation proves particularly valuable for combining hash components while maintaining good distribution properties.

class CustomHash
  def self.hash_combine(*values)
    values.reduce(0) do |hash, value|
      value_hash = value.hash
      hash ^= value_hash + 0x9e3779b9 + (hash << 6) + (hash >> 2)
    end
  end
  
  def self.fnv1a_hash(string)
    hash = 0x811c9dc5
    string.each_byte do |byte|
      hash ^= byte
      hash = (hash * 0x01000193) & 0xffffffff
    end
    hash
  end
end

hash1 = CustomHash.hash_combine("hello", 42, :symbol)
hash2 = CustomHash.fnv1a_hash("test string")

State machine implementations use bitwise flags to represent complex state combinations efficiently. Multiple states can coexist within single integers, with bitwise operations managing state transitions.

class StateMachine
  IDLE     = 0b0001
  RUNNING  = 0b0010
  PAUSED   = 0b0100
  ERROR    = 0b1000
  
  ACTIVE_STATES = RUNNING | PAUSED
  
  def initialize
    @state = IDLE
  end
  
  def add_state(state_flag)
    @state |= state_flag
  end
  
  def remove_state(state_flag)
    @state &= ~state_flag
  end
  
  def has_state?(state_flag)
    (@state & state_flag) != 0
  end
  
  def is_active?
    (@state & ACTIVE_STATES) != 0
  end
  
  def transition_to(new_state)
    @state = (@state & ERROR) | new_state
  end
  
  def state_description
    states = []
    states << "IDLE" if has_state?(IDLE)
    states << "RUNNING" if has_state?(RUNNING)
    states << "PAUSED" if has_state?(PAUSED)
    states << "ERROR" if has_state?(ERROR)
    states.join(" | ")
  end
end

machine = StateMachine.new
machine.transition_to(StateMachine::RUNNING)
machine.add_state(StateMachine::ERROR)
puts machine.state_description  # "RUNNING | ERROR"

Cryptographic applications use bitwise operations for key scheduling, substitution operations, and stream cipher implementations. The operations provide controlled bit diffusion and confusion properties essential for cryptographic security.

class SimpleStreamCipher
  def initialize(key)
    @key = key.bytes
    @key_index = 0
  end
  
  def encrypt_decrypt(data)
    result = []
    data.each_byte do |byte|
      key_byte = @key[@key_index % @key.length]
      encrypted_byte = byte ^ key_byte
      
      # Evolve key state using bitwise operations
      @key[@key_index % @key.length] = (key_byte << 1) ^ (key_byte >> 7) ^ encrypted_byte
      @key_index = (@key_index + 1) % @key.length
      
      result << encrypted_byte
    end
    result.pack('C*')
  end
end

cipher = SimpleStreamCipher.new("SECRET_KEY")
plaintext = "This is a secret message"
ciphertext = cipher.encrypt_decrypt(plaintext)
cipher2 = SimpleStreamCipher.new("SECRET_KEY")
decrypted = cipher2.encrypt_decrypt(ciphertext)

Performance & Memory

Bitwise operations execute significantly faster than arithmetic alternatives for specific use cases. Left and right shifts provide multiplication and division by powers of two with reduced computational overhead compared to general multiplication and division operations.

require 'benchmark'

def arithmetic_multiply(n, times)
  1_000_000.times { n * times }
end

def bitwise_multiply(n, power)
  1_000_000.times { n << power }
end

n = 1000
Benchmark.bm(20) do |x|
  x.report("arithmetic * 8:")     { arithmetic_multiply(n, 8) }
  x.report("bitwise << 3:")       { bitwise_multiply(n, 3) }
  x.report("arithmetic * 16:")    { arithmetic_multiply(n, 16) }
  x.report("bitwise << 4:")       { bitwise_multiply(n, 4) }
end

# Typical results show 2-3x speedup for bitwise operations

Memory efficiency improves dramatically when using bitwise operations to pack multiple values into single integers. Flag systems and enumeration sets benefit from reduced memory footprint and improved cache locality.

# Memory comparison: array vs bitfield for boolean flags
class BooleanArray
  def initialize(size)
    @flags = Array.new(size, false)
  end
  
  def set(index)
    @flags[index] = true
  end
  
  def get(index)
    @flags[index]
  end
end

class BitFieldFlags
  def initialize(size)
    @bits = 0
    @size = size
  end
  
  def set(index)
    @bits |= (1 << index)
  end
  
  def get(index)
    (@bits & (1 << index)) != 0
  end
end

# For 1000 boolean flags:
# BooleanArray: ~8000 bytes (8 bytes per boolean)
# BitFieldFlags: 8 bytes for small counts, scales by powers of two

Bit manipulation algorithms often demonstrate superior performance characteristics for specific problem domains. Population count operations (counting set bits) benefit from specialized bitwise techniques.

def population_count_naive(n)
  count = 0
  while n > 0
    count += 1 if (n & 1) == 1
    n >>= 1
  end
  count
end

def population_count_optimized(n)
  count = 0
  while n > 0
    n &= n - 1  # Clears lowest set bit
    count += 1
  end
  count
end

def population_count_lookup(n)
  # Pre-computed table for 8-bit values
  @lookup ||= (0..255).map { |i| i.to_s(2).count('1') }
  
  count = 0
  while n > 0
    count += @lookup[n & 0xFF]
    n >>= 8
  end
  count
end

# Benchmark results typically show:
# Optimized version: 3-5x faster than naive
# Lookup version: 8-12x faster for large numbers

Hash table implementations gain performance benefits from bitwise operations in key distribution and bucket selection algorithms. The operations provide fast modulo alternatives for power-of-two table sizes.

class BitwiseHashTable
  def initialize(initial_capacity = 16)
    @capacity = initial_capacity
    @mask = @capacity - 1  # Works only for power-of-two sizes
    @buckets = Array.new(@capacity) { [] }
    @size = 0
  end
  
  def put(key, value)
    index = hash_key(key) & @mask  # Fast modulo for power-of-two
    bucket = @buckets[index]
    
    existing = bucket.find { |k, v| k == key }
    if existing
      existing[1] = value
    else
      bucket << [key, value]
      @size += 1
      resize if @size > @capacity * 0.75
    end
  end
  
  def get(key)
    index = hash_key(key) & @mask
    pair = @buckets[index].find { |k, v| k == key }
    pair&.[](1)
  end
  
  private
  
  def hash_key(key)
    hash = key.hash
    # Bit mixing for better distribution
    hash ^= hash >> 16
    hash ^= hash >> 8
    hash ^= hash >> 4
    hash
  end
  
  def resize
    old_buckets = @buckets
    @capacity <<= 1  # Double capacity
    @mask = @capacity - 1
    @buckets = Array.new(@capacity) { [] }
    @size = 0
    
    old_buckets.each do |bucket|
      bucket.each { |key, value| put(key, value) }
    end
  end
end

Common Pitfalls

Two's complement arithmetic creates unexpected results when applying bitwise NOT to positive numbers. The NOT operator produces negative values due to sign bit manipulation, not simple bit inversion as expected from other programming contexts.

# Unexpected NOT behavior
positive = 5        # 0b00000101
result = ~positive  # -6, not 0b11111010 as might be expected

# The issue: Ruby uses arbitrary precision integers
# ~5 in two's complement becomes -6
# To get bit inversion in a fixed width:
def bit_invert_8bit(value)
  (~value) & 0xFF
end

bit_invert_8bit(5)  # => 250 (0b11111010)

Operator precedence creates subtle bugs when mixing bitwise operations with arithmetic or comparison operators. Bitwise operators have lower precedence than arithmetic operators, leading to incorrect evaluation order.

# Precedence pitfall
flags = 8
mask = 4
result = flags & mask == 4  # Wrong! Evaluates as: flags & (mask == 4)
# => 0, because (mask == 4) is true, and 8 & true gives unexpected results

# Correct approaches
result = (flags & mask) == 4  # => true
result = flags & mask != 0    # Check if any bits match

Sign extension during right shift operations produces counterintuitive results with negative numbers. Ruby preserves sign bits during arithmetic right shift, which differs from logical right shift behavior expected in unsigned contexts.

# Sign extension surprise
negative = -8    # Binary: ...11111000
shifted = negative >> 1   # -4, not large positive number

# The arithmetic right shift preserves the sign
# -8 >> 1 = -4 (fills with 1s from the left)
# Compare with other languages' unsigned right shift behavior

# To simulate unsigned right shift for specific bit widths:
def unsigned_right_shift_32bit(value, positions)
  (value & 0xFFFFFFFF) >> positions
end

Integer coercion rules create type errors when applying bitwise operations to non-integer types. Ruby requires integer operands and does not automatically convert floating-point numbers or other numeric types.

# Type coercion issues
float_val = 3.14
# int_val = float_val & 0xFF  # TypeError: no implicit conversion

# Must explicitly convert
int_val = float_val.to_i & 0xFF  # => 3

# Objects must implement to_int for coercion
class BitValue
  def initialize(value)
    @value = value
  end
  
  def to_int
    @value.to_i
  end
end

bit_obj = BitValue.new(42)
result = bit_obj & 15  # Works due to to_int implementation

Large integer operations can produce memory and performance issues when working with extremely large bit patterns. Ruby's arbitrary precision integers consume memory proportional to their magnitude, making bit operations on huge numbers expensive.

# Memory explosion with large integers
huge_number = 2 ** 100_000
# Each bit operation on huge_number requires significant memory

# Better approach: work with smaller chunks
def process_large_bitfield(large_int, chunk_size = 64)
  results = []
  while large_int > 0
    chunk = large_int & ((1 << chunk_size) - 1)
    results << process_chunk(chunk)
    large_int >>= chunk_size
  end
  results
end

def process_chunk(chunk)
  # Process 64-bit chunks efficiently
  chunk & 0xAAAAAAAAAAAAAAAA  # Example operation
end

Endianness assumptions break when serializing bitwise operation results across different systems. Ruby's internal integer representation may not match expected byte ordering for network or file protocols.

# Endianness pitfall
value = 0x12345678
bytes = [value].pack('N')  # Network (big-endian) byte order
# vs
bytes = [value].pack('V')  # VAX (little-endian) byte order

# The packed bytes represent the same number differently
# Network: [0x12, 0x34, 0x56, 0x78]
# VAX:     [0x78, 0x56, 0x34, 0x12]

# Ensure consistent byte ordering for portability
def to_big_endian_bytes(value, byte_count)
  bytes = []
  byte_count.times do |i|
    bytes.unshift((value >> (i * 8)) & 0xFF)
  end
  bytes
end

Mask overflow occurs when bit manipulation operations exceed intended bit width boundaries. Operations may set bits beyond the expected range, corrupting adjacent data fields in packed structures.

# Mask overflow problem
def set_field_value(packed_data, field_start, field_width, value)
  # Dangerous: doesn't check if value fits in field_width
  mask = ((1 << field_width) - 1) << field_start
  packed_data = (packed_data & ~mask) | (value << field_start)
end

# Safe version with bounds checking
def set_field_value_safe(packed_data, field_start, field_width, value)
  max_value = (1 << field_width) - 1
  raise ArgumentError, "Value #{value} too large for #{field_width}-bit field" if value > max_value
  raise ArgumentError, "Negative values not supported" if value < 0
  
  mask = max_value << field_start
  (packed_data & ~mask) | ((value & max_value) << field_start)
end

Reference

Bitwise Operators

Operator Syntax Description Example Result
AND a & b Returns 1 if both bits are 1 12 & 10 8
OR a + b Returns 1 if either bit is 1 12 + 10 14
XOR a ^ b Returns 1 if bits differ 12 ^ 10 6
NOT ~a Inverts all bits ~12 -13
Left shift a << n Shifts bits left by n positions 12 << 2 48
Right shift a >> n Shifts bits right by n positions 12 >> 2 3

Common Bit Manipulation Patterns

Operation Formula Description
Set bit n value OR (1 << n) Sets the nth bit to 1
Clear bit n value & ~(1 << n) Sets the nth bit to 0
Toggle bit n value ^ (1 << n) Flips the nth bit
Check bit n (value & (1 << n)) != 0 Tests if nth bit is set
Clear lowest set bit value & (value - 1) Removes rightmost 1 bit
Isolate lowest set bit value & (-value) Keeps only rightmost 1 bit
Check power of two (value & (value - 1)) == 0 True if value is power of 2
Extract bit range (value >> i) & mask Extracts bits starting at position i

Binary Constants and Literals

Format Example Decimal Value Description
Binary 0b1010 10 Binary literal with 0b prefix
Octal 0o12 10 Octal literal with 0o prefix
Hexadecimal 0xA 10 Hexadecimal literal with 0x prefix

Method Availability

Method Class Parameters Returns Description
& Integer other Integer Integer Bitwise AND operation
+ Integer other Integer Integer Bitwise OR operation
^ Integer other Integer Integer Bitwise XOR operation
~ Integer none Integer Bitwise NOT operation
<< Integer count Integer Integer Left shift operation
>> Integer count Integer Integer Right shift operation
[] Integer bit_position Integer 0 or 1 Returns bit at position
bit_length Integer none Integer Number of bits in absolute value
nobits? Integer mask Integer Boolean True if no bits in mask are set
allbits? Integer mask Integer Boolean True if all bits in mask are set
anybits? Integer mask Integer Boolean True if any bits in mask are set

Conversion Methods

Method Returns Description
to_s(2) String Binary string representation
to_s(8) String Octal string representation
to_s(16) String Hexadecimal string representation
Integer(string, base) Integer Parse string in specified base

Performance Characteristics

Operation Time Complexity Space Complexity Notes
AND, OR, XOR O(min(log a, log b)) O(log result) Dependent on operand sizes
NOT O(log a) O(log a) Linear in bit length
Left shift, Right shift O(log a) O(log result) May create larger integers
Bit test O(1) O(1) Constant time access
Population count O(log n) O(1) Using bit manipulation tricks

Error Types

Error Condition Example
TypeError Non-integer operand without to_int 3.14 & 5
ArgumentError Negative shift count 5 << -1
NoMethodError Bitwise operation on unsupported type "string" & 5

Compatibility Notes

Ruby Version Changes
2.4+ Added Integer#allbits?, Integer#anybits?, Integer#nobits?
2.1+ Unified Fixnum and Bignum into Integer
1.9+ Consistent two's complement behavior