Numbers · Lesson 16.4

Bitwise

Source
Work on individual bits with & | ^ ~, and use masks to set, clear, flip and test flags packed into one byte.

Underneath, every integer is a row of bits. Arithmetic treats that row as one number; the bitwise operators treat it as a row of separate yes-or-no switches and work on each position on its own. That makes them the tool for packing several small facts into one value — eight permissions in a single byte, a set of options in one argument — and for reading them back out.

Four operators, one bit at a time

OperatorNameA result bit is 1 when…
a & bANDboth bits are 1
a | bOReither bit is 1
a ^ bXORexactly one of the two bits is 1
~aNOTthe bit in a is 0 — every bit flips

The program prints them side by side. {:08b} formats a number in binary, padded with zeros to eight digits, so the columns line up:

let a: uint8 = 0b1100_1010;
let b: uint8 = 0b1010_0110;
PrintLine("a      {:08b}", a);
PrintLine("b      {:08b}", b);
PrintLine("a & b  {:08b}", a & b);
PrintLine("a | b  {:08b}", a | b);
PrintLine("a ^ b  {:08b}", a ^ b);
PrintLine("~a     {:08b}", ~a);
a      11001010
b      10100110
a & b  10000010
a | b  11101110
a ^ b  01101100
~a     00110101

Read any column top to bottom and the rule of the table holds. ~ depends on the width of its operand: ~ of a uint8 flips eight bits, ~ of a uint32 flips thirty-two. Unsigned types are the usual choice for bit work, because no bit doubles as a sign.

Flags and masks

A mask is a value whose set bits pick out the positions you care about. Give each flag its own bit, and one byte holds eight of them. Written in binary, the constants show which bit each one owns:

const Read: uint8 = 0b001;
const Write: uint8 = 0b010;
const Execute: uint8 = 0b100;

Four idioms cover almost everything done with flags:

GoalIdiomWhy it works
Set a flagflags |= WriteOR turns that bit on and leaves the rest alone
Clear a flagflags &= ~Read~Read has every bit but one set; AND keeps those
Flip a flagflags ^= ExecuteXOR with 1 flips a bit, XOR with 0 keeps it
Test a flag(flags & Write) != 0AND keeps only that bit; non-zero means it was set

In the program they run one after another on flags:

var flags: uint8 = Read;
flags |= Write;
flags ^= Execute;
flags &= ~Read;

The byte goes 001 → 011 → 111 → 110: it started with Read, gained Write, gained Execute by a flip, and lost Read.

flowchart LR
    s["001<br/>Read"] -- "set Write" --> w["011"]
    w -- "flip Execute" --> x["111"]
    x -- "clear Read" --> r["110<br/>Write, Execute"]

Testing flags

Testing asks whether a bit survives the mask:

PrintLine("can write?     {}", (flags & Write) != 0);
PrintLine("can read?      {}", (flags & Read) != 0);

A mask with several bits tests them together. != 0 would mean "any of them"; comparing with the mask itself means "all of them":

let both = Write | Execute;
PrintLine("write and run? {}", (flags & both) == both);

Watch the precedence

&, | and ^ bind more loosely than == and !=. So flags & Write == 0 reads as flags & (Write == 0) — a byte AND a boolean — and the compiler rejects it. Parenthesise the mask every time: (flags & Write) == 0.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// The bitwise operators treat an integer as a row of bits and work on each position separately.
// `a & b` keeps a bit only where both have it, `a | b` where either has it, and `a ^ b` where
// exactly one has it. `~a` flips every bit, so its result depends on the width: `~` of a `uint8`
// flips eight bits, of a `uint32` thirty-two.
//
// Their everyday use is the mask: a value whose set bits pick out the positions you care about.
// One byte can then hold eight yes-or-no flags, and four idioms cover almost everything done with
// them: `|` sets a flag, `& ~` clears it, `^` flips it, and `&` tests it.
//
// Watch the precedence. `&`, `|` and `^` bind more loosely than `==`, so `flags & Write == 0` reads
// as `flags & (Write == 0)` and is rejected for mixing a `uint8` with a `bool`. Parenthesize the
// mask: `(flags & Write) == 0`.
import Io::PrintLine;

// One bit per permission. Written in binary, the masks show which bit each one owns.
const Read: uint8 = 0b001;
const Write: uint8 = 0b010;
const Execute: uint8 = 0b100;

func Main() -> int {
    // The four operators, side by side.
    let a: uint8 = 0b1100_1010;
    let b: uint8 = 0b1010_0110;
    PrintLine("a      {:08b}", a);
    PrintLine("b      {:08b}", b);
    PrintLine("a & b  {:08b}", a & b);
    PrintLine("a | b  {:08b}", a | b);
    PrintLine("a ^ b  {:08b}", a ^ b);
    PrintLine("~a     {:08b}", ~a);
    PrintLine("");

    // Masks at work on a set of flags.
    var flags: uint8 = Read;
    PrintLine("start          {:03b}", flags);

    flags |= Write;
    PrintLine("set Write      {:03b}", flags);

    flags ^= Execute;
    PrintLine("flip Execute   {:03b}", flags);

    flags &= ~Read;
    PrintLine("clear Read     {:03b}", flags);

    PrintLine("can write?     {}", (flags & Write) != 0);
    PrintLine("can read?      {}", (flags & Read) != 0);

    // A mask with several bits tests them together.
    let both = Write | Execute;
    PrintLine("write and run? {}", (flags & both) == both);
    return 0;
}

Run it

cd Examples/Numbers/Bitwise
rux run
a      11001010
b      10100110
a & b  10000010
a | b  11101110
a ^ b  01101100
~a     00110101

start          001
set Write      011
flip Execute   111
clear Read     110
can write?     true
can read?      false
write and run? true

Common mistakes

Leaving out the parentheses.
flags & Write == 0 groups as flags & (Write == 0) and fails with error: operator '&' cannot combine left operand 'uint8' with right operand 'bool8'. Write (flags & Write) == 0.
Using ! to invert a mask.
! is logical NOT, for booleans only: flags & !Write fails with error: operator '!' requires a bool operand, but found 'uint8'. The bitwise NOT is ~: flags & ~Write.
Using a masked value as a condition.
Rux does not treat a non-zero number as true. if flags & Write { … } fails with error: condition for 'if' must have type 'bool', but found 'uint8'. Compare it: if (flags & Write) != 0 { … }.

Try it yourself

  1. Add a fourth flag, Delete: uint8 = 0b1000, set it, and print flags with {:04b}.
  2. Write func Has(flags: uint8, mask: uint8) -> bool that returns whether every bit of mask is set in flags.
  3. XOR a value with the same mask twice. What do you get back, and why?
  4. Change a and b to uint32 and print ~a with {:032b}. How many bits flipped this time?

Learn more