Bit Manipulation
Bit twiddling is the heart of systems programming: flags packed into a single word, hardware registers, bitsets. The task is identical in all seven languages - start from a value, set a bit with | (1 << n), clear it with & ~(1 << n), test it with (x >> n) & 1, then print the result in binary and hex. The integer lives in a register or on the stack, so this topic touches no heap - it's a pure look at each language's bitwise operators (&, |, ^, ~, <<, >>) and the helpers some of them add: C++'s std::bitset, Zig's @shlExact/std.math, Odin's bit_set, and Forth's hand-rolled masks.
#include <stdio.h>
#include <stdint.h>
/* Bit helpers as the canonical C macros. n is the bit index (0 = LSB). */
#define SET_BIT(x, n) ((x) | (1u << (n))) /* OR in a 1 */
#define CLEAR_BIT(x, n) ((x) & ~(1u << (n))) /* AND in a 0 (mask is ...1101...) */
#define TEST_BIT(x, n) (((x) >> (n)) & 1u) /* shift the bit down, mask it off */
/* Print the low 8 bits of v as binary, MSB first. */
static void print_bin8(uint8_t v) {
for (int i = 7; i >= 0; i--)
putchar((v >> i) & 1u ? '1' : '0');
putchar('\n');
}
int main(void) {
uint8_t x = 0x0A; /* 0000 1010 */
x = SET_BIT(x, 0); /* turn on bit 0 -> 0000 1011 */
x = CLEAR_BIT(x, 3); /* turn off bit 3 -> 0000 0011 */
print_bin8(x); /* 00000011 */
printf("hex = 0x%02X\n", x); /* 0x03 */
printf("bit 1 set? %d\n", TEST_BIT(x, 1)); /* 1 */
return 0;
}The three idioms are textbook C macros: set with | (1<<n), clear with & ~(1<<n), test with (x>>n)&1. x is a stack uint8_t and the masks are computed in registers, so bit work here costs zero allocations - there is no heap to malloc or free.
#include <bitset>
#include <cstdint>
#include <iostream>
int main() {
std::uint8_t x = 0x0A; // 0000 1010
// Raw operators, just like C: set, clear, test.
x |= (1u << 0); // set bit 0 -> 0000 1011
x &= ~(1u << 3); // clear bit 3 -> 0000 0011
bool bit1 = (x >> 1) & 1u; // test bit 1
// The STL helper: std::bitset wraps a fixed-width word with named ops.
std::bitset<8> bits{x}; // value lives inline, no heap
bits.set(2); // bits.set/reset/test/flip read clearly
std::cout << "bits: " << bits << '\n'; // 00000111
std::cout << "hex: 0x" << std::hex << (int)x << '\n'; // 0x3
std::cout << "bit 1? " << bit1 << '\n'; // 1
std::cout << "count: " << bits.count() << '\n'; // popcount = 3
}Modern C++ still uses the raw |/&/~/>> operators, but std::bitset<N> is the idiomatic helper: .set(), .reset(), .test(), .flip(), and .count() (popcount) read far clearer than masks. bitset<8> stores its bits inline (no heap, no new/delete), so there is nothing for RAII to clean up.
// Top-level code runs in TempleOS -- no main() needed.
// HolyC's operators are C's: & | ^ ~ << >>.
U0 BitPrint(U8 v) { // print low 8 bits, MSB first
I64 i;
for (i = 7; i >= 0; i--)
Print("%d", (v >> i) & 1);
Print("\n");
}
U8 x = 0x0A; // 0000 1010
x |= (1 << 0); // set bit 0 -> 0000 1011
x &= ~(1 << 3); // clear bit 3 -> 0000 0011
BitPrint(x); // 00000011
Print("hex = 0x%02X\n", x); // 0x03
Print("bit 1 set? %d\n", (x >> 1) & 1); // 1
HolyC inherits C's bitwise operators verbatim, so set / clear / test are the same |, & ~, and >>/& expressions. x is a plain U8 on the task's stack - the per-task heap is never touched, so there is no MAlloc/Free. Runs only on TempleOS.
const std = @import("std");
pub fn main() void {
var x: u8 = 0x0A; // 0000 1010
// Zig shifts require the shift AMOUNT to be a small unsigned type (u3 for u8),
// which is checked at comptime -- shifting past the width is a bug, not UB.
x |= @as(u8, 1) << 0; // set bit 0 -> 0000 1011
x &= ~(@as(u8, 1) << 3); // clear bit 3 -> 0000 0011
const bit1: u1 = @truncate(x >> 1); // test bit 1
// std.math has typed helpers; @popCount is a builtin.
std.debug.print("bin = {b:0>8}\n", .{x}); // 00000011
std.debug.print("hex = 0x{X:0>2}\n", .{x}); // 0x03
std.debug.print("bit 1? {d}\n", .{bit1}); // 1
std.debug.print("popcount = {d}\n", .{@popCount(x)}); // 2
}Zig makes bit widths explicit: a shift amount for a u8 must fit in a u3, and the literal 1 is widened with @as(u8, 1) so the mask is the right type - overflow is a comptime/safety error, never silent UB. @popCount is a builtin and {b}/{X} format binary/hex. x is a stack var, so no allocator is involved.
use fmt;
// Hare's bitwise operators: & | ^ ~ << >>.
export fn main() void = {
let x: u8 = 0x0A; // 0000 1010
x |= (1u8 << 0); // set bit 0 -> 0000 1011
x &= ~(1u8 << 3); // clear bit 3 -> 0000 0011
const bit1: u8 = (x >> 1) & 1; // test bit 1
// Print the low 8 bits MSB-first.
for (let i = 7i; i >= 0; i -= 1) {
fmt::print((x >> (i: u8)) & 1)!;
};
fmt::println()!;
fmt::printfln("hex = 0x{:x}", x)!; // 0x3
fmt::printfln("bit 1? {}", bit1)!; // 1
};Hare's bitwise operators mirror C (&, |, ^, ~, <<, >>), and untyped integer literals like 1u8 carry their type explicitly. The shift index is cast to u8 to match x. Everything is a stack let/const with no alloc/free; the trailing ! propagates any I/O error rather than ignoring it.
package main
import "core:fmt"
main :: proc() {
x: u8 = 0x0A // 0000 1010
// Raw operators: Odin uses ~ for XOR and &~ for AND-NOT (bit clear).
x |= (1 << 0) // set bit 0 -> 0000 1011
x &~= (1 << 3) // clear bit 3 -> 0000 0011 (&~ is Odin's clear idiom)
bit1 := (x >> 1) & 1 // test bit 1
// Odin's first-class helper: bit_set is a typed set of enum/range members,
// stored as a single integer -- add/remove/test read like set operations.
Flag :: enum {A, B, C, D}
flags: bit_set[Flag]
flags += {.A, .C} // set bits A and C
flags -= {.A} // clear bit A
fmt.printf("bin = %08b\n", x) // 00000011
fmt.printf("hex = 0x%02X\n", x) // 0x03
fmt.println("bit 1?", bit1) // 1
fmt.println("flags:", flags, "C in?", .C in flags) // {C} true
}Odin spells XOR as ~ and AND-NOT (clear) as &~, so x &~= mask is the idiomatic bit-clear. Its standout helper is bit_set[T] - a set of enum members packed into one integer, manipulated with += {…} / -= {…} and tested with in. x and flags are stack values, so the implicit context.allocator is untouched.
\ Forth has the bitwise primitives AND OR XOR INVERT LSHIFT RSHIFT.
\ (INVERT is bitwise NOT; there is no built-in ~, &~, etc.)
\ Build the three helpers as words; the value rides on the data stack.
: BIT ( n -- mask ) 1 SWAP LSHIFT ; \ 1 shifted left n times
: SET ( x n -- x' ) BIT OR ; \ OR in the mask
: CLEAR ( x n -- x' ) BIT INVERT AND ; \ AND with ~mask
: TEST ( x n -- f ) RSHIFT 1 AND ; \ shift down, mask low bit
\ Print the low 8 bits of x, MSB first.
: .BIN8 ( x -- )
8 0 DO ( loop i = 0..7 )
DUP 7 I - RSHIFT 1 AND . \ print bit (7-i)
LOOP DROP CR ;
10 \ x = 0x0A = 0000 1010 on the stack
0 SET \ set bit 0 -> 0000 1011
3 CLEAR \ clear bit 3 -> 0000 0011
DUP .BIN8 \ 0 0 0 0 0 0 1 1
DUP 1 TEST . \ test bit 1 -> 1
HEX . DECIMAL CR \ print value in hex -> 3Forth gives you the raw primitives AND OR XOR INVERT LSHIFT RSHIFT and nothing else, so set/clear/test are defined as words that operate on the value already on the data stack - INVERT AND is the clear idiom (no &~). HEX/DECIMAL switch the numeric base for . rather than formatting a string. No variables, no heap: every intermediate lives on the stack, so nothing is allocated or freed.