Project

General

Profile

Actions

Feature #22118

open

Introduce Basic Bit Operations into String

Feature #22118: Introduce Basic Bit Operations into String

Added by hasumikin (hitoshi hasumi) about 1 month ago. Updated 9 days ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:125775]

Description

This PR implements basic bit operations into String, incorporating revisions made following the discussion (esp. #note-5)

PR URL: https://github.com/ruby/ruby/pull/17353

Single-bit read and mutation

  • String#bit_get(offset, lsb_first: true) -> 1 | 0 | nil
  • String#bit_set?(offset, lsb_first: true) -> true | false | nil
  • String#bit_set(offset, lsb_first: true) -> self
  • String#bit_clear(offset, lsb_first: true) -> self
  • String#bit_flip(offset, lsb_first: true) -> self

Note for bit_set/bit_clear/bit_flip: As these methods are destructive-only (they have no non-destructive counterpart), no ! is added. The ! suffix is meant to distinguish the dangerous member of a destructive/non-destructive pair, following the setbyte precedent.

Bitwise operators

  • String#bitwise_not -> String
  • String#bitwise_not! -> self
  • String#bitwise_and(other) -> String
  • String#bitwise_and!(other) -> self
  • String#bitwise_or(other) -> String
  • String#bitwise_or!(other) -> self
  • String#bitwise_xor(other) -> String
  • String#bitwise_xor!(other) -> self

Popcount

  • String#bit_count -> Integer

API Contracts

1. Bit-order keyword

lsb_first: true (default)

Within each byte, offset = 0 is the LSB. Numbering proceeds upward through byte[0] and then continues at the LSB of byte[1]:

        byte[0]                    byte[1]
offset: 7  6  5  4  3  2  1  0     15 14 13 12 11 10 9  8
bit:    b7 b6 b5 b4 b3 b2 b1 b0    b7 b6 b5 b4 b3 b2 b1 b0
                             ^                          ^
                             LSB                        LSB

offset = byte_index * 8 + bit_in_byte

lsb_first: false for MSB-first

Byte order is preserved (byte[0] is still first), but within each byte numbering starts at the MSB:

        byte[0]                    byte[1]
offset: 0  1  2  3  4  5  6  7     8  9  10 11 12 13 14 15
bit:    b7 b6 b5 b4 b3 b2 b1 b0    b7 b6 b5 b4 b3 b2 b1 b0
        ^                          ^
        MSB                        MSB

offset = byte_index * 8 + (7 - bit_in_byte)

The keyword only controls bit numbering within each byte. Byte order itself is unchanged.

LSB-first is the more common convention for general in-memory bitmap use, including formats and APIs such as Apache Arrow validity bitmaps, ext4 block bitmaps, Roaring bitmaps, Linux/BSD bitmap APIs, and hardware register descriptions.
MSB-first is also needed for domains such as RFC-style network protocol diagrams, PNG low-bit-depth scanlines, BitTorrent bitfields, and some compressed bit streams.
This is the reason that we define lsb_first: true as the default.

The bit-order keyword must be accepted by all bit_* methods with the same default; a getter and a setter disagreeing on bit addressing would be disastrous.

One temporary exception: Since the bit_count introduced in this version counts the population of the entire string without taking arguments, it does not accept a bit-order keyword. If arguments for specifying a range are introduced in the future, the bit-order keyword should be introduced at that time.

2. Out-of-range behavior

Read methods: String#bit_get and String#bit_set? return nil when the bit offset is out of range.

On the other hand, mutating methods such as String#bit_set, String#bit_clear, and String#bit_flip raise IndexError for an out-of-range bit offset. This follows the same general distinction as read-like access versus write-like access: reading a missing position can return nil, but mutating a missing position should not silently do nothing.

If the bit offset is too large to be represented internally, all of these methods raise ArgumentError before applying the read/write out-of-range rule. This bound is specified explicitly because the representable offset range may differ between Ruby implementations.

3. Negative bit offsets

Negative bit offsets are rejected with IndexError.

Although Ruby often uses negative indices to count from the end, combining negative bit offsets with the lsb_first: true/false mechanism would make the numbering rule harder to explain and reason about. The API keeps bit offsets as non-negative flat positions from the beginning of the string.

4. Length mismatch for binary bitwise operations

String#bitwise_and, String#bitwise_or, String#bitwise_xor, and their ! variants require both operands to have the same byte size. If the byte sizes differ, they raise ArgumentError.

This avoids implicit truncation or zero-padding. Since these methods operate on strings as fixed-size bitmaps, requiring equal sizes makes the operation explicit and predictable.

Silent zero-extension would let corrupted results flow downstream in the typical use cases (checksums, masks); callers who want padding can ljust explicitly.

5. Encoding

All bit operations treat the receiver as a byte sequence. For non-destructive methods that return a String, the return value's encoding is set to Encoding::BINARY. Destructive methods do not alter the receiver's encoding, consistent with setbyte, which likewise lets a byte-level mutation produce an encoding-invalid string without changing the encoding.


Related issues 1 (0 open1 closed)

Related to Ruby - Feature #22082: Introduce Bit Operations into StringClosedActions
Actions

Also available in: PDF Atom