bits

tape
import bits from "bits";

bits works on the bits and bytes of unsigned integers: counting bits, rotating, swapping byte order, and reading or writing fixed-width integers in a byte buffer. It is a pure t0 module: it allocates nothing, has no state, and works in every profile.

Counts and offsets are i64, like .len. The functions exist for the widths shown in their names; for a signed value, cast it (x as u64).

Counting bits

FunctionReturns
popcount_u64(x: u64) -> i64the number of 1 bits (0 to 64)
popcount_u32(x: u32) -> i64the number of 1 bits (0 to 32)
clz_u64(x: u64) -> i64the 0 bits above the highest 1 bit; 64 when x is 0
clz_u32(x: u32) -> i64the same for 32 bits; 32 when x is 0
ctz_u64(x: u64) -> i64the 0 bits below the lowest 1 bit; 64 when x is 0
ctz_u32(x: u32) -> i64the same for 32 bits; 32 when x is 0
tape
io.println("{bits.popcount_u64(0xF0F0)}");      // 8
io.println("{bits.clz_u64(1)}");                // 63
io.println("{bits.clz_u64(0)}");                // 64
io.println("{bits.ctz_u64(0x100)}");            // 8
io.println("{bits.ctz_u32(0)}");                // 32

// Bits needed to hold n: 64 - clz, which is 0 for n == 0
let n: u64 = 1000;
let width: i64 = 64 - bits.clz_u64(n);          // 10

Rotating

FunctionReturns
rotl_u64(x: u64, n: i64) -> u64x rotated left by n bits
rotr_u64(x: u64, n: i64) -> u64x rotated right by n bits
rotl_u32(x: u32, n: i64) -> u32x rotated left by n bits
rotr_u32(x: u32, n: i64) -> u32x rotated right by n bits

Bits that leave one end come back in at the other. Any n works: it is taken modulo the width, so rotating by 64 (or 32) changes nothing and a negative n rotates the other way.

tape
let a: u64 = bits.rotl_u64(0x8000000000000001, 4);   // 0x18
let b: u64 = bits.rotr_u64(1, 1);                    // 0x8000000000000000
let c: u32 = bits.rotl_u32(0x12345678, 8);           // 0x34567812
let d: u32 = bits.rotr_u32(0x12345678, 8);           // 0x78123456

Swapping byte order

FunctionReturns
bswap_u16(x: u16) -> u16x with its 2 bytes reversed
bswap_u32(x: u32) -> u32x with its 4 bytes reversed
bswap_u64(x: u64) -> u64x with its 8 bytes reversed
tape
let s: u16 = bits.bswap_u16(0x1234);                 // 0x3412
let w: u32 = bits.bswap_u32(0x12345678);             // 0x78563412
let q: u64 = bits.bswap_u64(0x0102030405060708);     // 0x0807060504030201

To read or write integers in a byte buffer, use the loads and stores below instead: they don’t depend on the machine’s byte order.

Reading integers from bytes

FunctionReads
load_u16_le(src: []const u8, off: i64) -> u162 bytes at off, least significant first
load_u16_be(src: []const u8, off: i64) -> u162 bytes at off, most significant first
load_u32_le(src: []const u8, off: i64) -> u324 bytes, little-endian
load_u32_be(src: []const u8, off: i64) -> u324 bytes, big-endian
load_u64_le(src: []const u8, off: i64) -> u648 bytes, little-endian
load_u64_be(src: []const u8, off: i64) -> u648 bytes, big-endian

off can be any position; there is no alignment requirement. A load traps with offset out of range if off is negative or the bytes run past the end of src.

tape
// The first chunk of a PNG file: its length is a big-endian u32 at offset 8
let header: []const u8 = "\x89PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR";
let len: u32 = bits.load_u32_be(header, 8);          // 13

let le: []const u8 = "\x34\x12";
let v: u16 = bits.load_u16_le(le, 0);                // 0x1234

Writing integers to bytes

FunctionWrites
store_u16_le(dst: []u8, off: i64, v: u16)v as 2 bytes at off, least significant first
store_u16_be(dst: []u8, off: i64, v: u16)2 bytes, most significant first
store_u32_le(dst: []u8, off: i64, v: u32)4 bytes, little-endian
store_u32_be(dst: []u8, off: i64, v: u32)4 bytes, big-endian
store_u64_le(dst: []u8, off: i64, v: u64)8 bytes, little-endian
store_u64_be(dst: []u8, off: i64, v: u64)8 bytes, big-endian

Only those bytes of dst change. A store traps with offset out of range if off is negative or the bytes would run past the end of dst, and then writes nothing.

tape
var buf: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0];
bits.store_u16_be(buf, 0, 0xCAFE);       // buf[0] == 0xCA, buf[1] == 0xFE
bits.store_u32_le(buf, 4, 0x01020304);   // buf[4] == 4, buf[7] == 1
bits.store_u64_be(buf, 0, 42);
let back: u64 = bits.load_u64_be(buf, 0);   // 42

Constant time

The rotations, byte swaps, loads and stores are @constant_time: they never branch on, or index by, the bytes and values they work on, so constant-time code (cryptography) can call them with secrets. Their offset parameters are @public; the rotation count may be secret. The counting functions are not marked.

Last modified: