Skip to content

Bitwise Calculator — Zig source

Perform AND, OR, XOR, NOT, shifts and rotates on 8/16/32/64-bit values with exact bigint math. Enter operands in binary, octal, decimal or hex and read the result in every base plus a live bit grid. Runs 100% in your browser.

This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

// =============================================================================
//  zig.zig — CosmoDev polyglot showcase port of the `bitwise` tool
//  -----------------------------------------------------------------------------
//  Language : Zig (0.13, standard library only)
//  Source:   ported from src/lib/bitwise.ts (the canonical, live TypeScript
//             lib); mirrors src/tool-sources/bitwise/{python.py,rust.rs}
//  License  : display source — part of CosmoDev's polyglot tool pages
//             (dev.cosmolabs.org). Shown verbatim alongside the JS/TS/Go/Rust/
//             Python ports and the other language ports.
//  -----------------------------------------------------------------------------
//  Pure, deterministic bitwise calculator. Zero deps. Operands are interpreted
//  as width-bit two's-complement values: any integer is normalized to the
//  half-open range [0, 2^width) before an operation, and every result is masked
//  back into that range — so the returned integer is always the unsigned
//  bit-pattern of the width-bit result.
//
//  Zig note: the TS source uses arbitrary-precision `bigint`. Zig has built-in
//  i128/u128, so signed parsing uses i128 and masked results use u128 — the
//  same approach as the Rust port. This is exact for every supported width
//  (8/16/32/64) and for all shift intermediates. Only literals beyond ~1.7e38
//  diverge from the TS lib (a clean error.ValueOutOfRange), far outside any
//  realistic bitwise-calculator input.
//
//  Zig errors cannot carry payloads, so the Rust port's messages become the
//  tagged error names below (the base is recoverable from the argument).
//  @"and"/@"or"/@"not" are quoted because the bare spellings are (or risk
//  becoming) Zig keywords.
// =============================================================================

const std = @import("std");

/// Numeric radix used for parsing and formatting.
pub const Base = enum {
    bin,
    oct,
    dec,
    hex,

    /// Positional radix (2/8/10/16).
    pub fn radix(self: Base) u8 {
        return switch (self) {
            .bin => 2,
            .oct => 8,
            .dec => 10,
            .hex => 16,
        };
    }

    /// Lowercase digit alphabet valid for this base.
    pub fn digits(self: Base) []const u8 {
        return switch (self) {
            .bin => "01",
            .oct => "01234567",
            .dec => "0123456789",
            .hex => "0123456789abcdef",
        };
    }

    /// Lowercase name, for diagnostics that match the TS lib's messages.
    pub fn lower(self: Base) []const u8 {
        return switch (self) {
            .bin => "bin",
            .oct => "oct",
            .dec => "dec",
            .hex => "hex",
        };
    }
};

/// Bit-width of the virtual register (8/16/32/64).
pub const Width = u32;

/// Supported bitwise operation. `.not` is unary on `a`; the rest are binary,
/// with `b` as the shift/rotate count for the shift/rotate ops.
pub const Op = enum {
    @"and",
    @"or",
    xor,
    @"not",
    shl,
    shr,
    rol,
    ror,
};

/// Parse failure: empty input (`EmptyValue`, also covers a bare "-"), a digit
/// outside the requested base (`InvalidDigit`), or a literal that overflows
/// i128 (`ValueOutOfRange`).
pub const ParseError = error{
    EmptyValue,
    InvalidDigit,
    ValueOutOfRange,
};

/// Parse a numeric string in `base` into a signed integer. Strips 0x/0b/0o
/// prefixes and an optional leading sign. The raw signed value is returned
/// (no width normalization); callers fold it into a field via `normalize` /
/// `bitwise`.
pub fn parse(value: []const u8, base: Base) ParseError!i128 {
    const trimmed = std.mem.trim(u8, value, " \t\n\r");
    if (trimmed.len == 0)
        return error.EmptyValue;

    // Peel off an optional leading '-' so negative literals parse correctly.
    var negative = false;
    var body: []const u8 = trimmed;
    if (body[0] == '-') {
        negative = true;
        body = body[1..];
    }
    if (body.len == 0) // a bare "-"
        return error.EmptyValue;

    // Strip each base prefix in turn (0x, then 0b, then 0o) from a lowered
    // copy — mirrors the reference's chained leading-prefix removal.
    // Matching against the lowercased copy keeps "0xFF" valid.
    var lbuf: [128]u8 = undefined;
    if (body.len > lbuf.len)
        return error.ValueOutOfRange; // cannot fit i128 anyway
    const lowered = std.ascii.lowerString(&lbuf, body);
    var digit_str = lowered;
    const prefixes = [_][]const u8{ "0x", "0b", "0o" };
    for (prefixes) |prefix| {
        if (std.mem.startsWith(u8, digit_str, prefix))
            digit_str = digit_str[prefix.len..];
    }
    if (digit_str.len == 0)
        return error.EmptyValue;

    // Horner's method over the digit alphabet, with checked arithmetic:
    // an over-wide literal reports a clean error instead of overflowing.
    const allowed = base.digits();
    const radix: i128 = base.radix();
    var acc: i128 = 0;
    for (digit_str) |ch| {
        const idx = std.mem.indexOfScalar(u8, allowed, ch) orelse
            return error.InvalidDigit;
        const digit: i128 = idx;
        const mul = @mulWithOverflow(acc, radix);
        if (mul[1] != 0)
            return error.ValueOutOfRange;
        const add = @addWithOverflow(mul[0], digit);
        if (add[1] != 0)
            return error.ValueOutOfRange;
        acc = add[0];
    }

    return if (negative) -acc else acc;
}

/// Mask for a `width`-bit field: 2^width - 1. (Valid for width <= 127.)
pub fn mask(width: Width) u128 {
    return (@as(u128, 1) << @intCast(width)) - 1;
}

/// Normalize any signed value to its unsigned width-bit two's-complement
/// value, i.e. into the half-open range [0, 2^width) — e.g. -1 at width 8
/// yields 255. The reference computes ((n % m) + m) % m with m = 2^width;
/// Zig's @mod is floored (result takes the divisor's sign), so a single
/// application over a positive divisor produces the same canonical value.
pub fn normalize(n: i128, width: Width) u128 {
    const m: i128 = @as(i128, 1) << @intCast(width); // 2^width (fits i128 for width <= 126)
    return @intCast(@mod(n, m));
}

/// Format `n` in `base`, zero-padded to at least `min_digits` digits, into a
/// freshly allocated string. Negatives carry a leading '-' and format their
/// magnitude. (The third arg is a minimum digit count — the binary rendering
/// of a width-bit value needs exactly `width` digits.)
///
/// Manual base conversion keeps parity with the sibling ports and yields the
/// lowercase digits of the TS `bigint.toString(radix)` output.
pub fn format(alloc: std.mem.Allocator, n: i128, base: Base, min_digits: usize) std.mem.Allocator.Error![]u8 {
    if (n < 0) {
        // Format the magnitude and prepend the sign. The (-(n+1))+1 form
        // negates safely even at i128 min.
        const mag: u128 = @intCast(-(n + 1));
        const body = try format_unsigned(alloc, mag + 1, base, min_digits);
        defer alloc.free(body);
        const out = try alloc.alloc(u8, body.len + 1);
        out[0] = '-';
        @memcpy(out[1..], body);
        return out;
    }
    return format_unsigned(alloc, @intCast(n), base, min_digits);
}

// Extract digits LSB-first; the loop renders "0" for zero naturally.
fn format_unsigned(alloc: std.mem.Allocator, v_in: u128, base: Base, min_digits: usize) std.mem.Allocator.Error![]u8 {
    const alphabet = "0123456789abcdef";
    const radix = base.radix();

    var tmp: [128]u8 = undefined; // a 128-bit max is 128 binary digits
    var nd: usize = 0;
    var v = v_in;
    while (true) {
        tmp[nd] = alphabet[@intCast(v % radix)];
        nd += 1;
        v /= radix;
        if (v == 0)
            break;
    }

    const pad = min_digits -| nd; // saturating: no padding when already long enough
    const out = try alloc.alloc(u8, pad + nd);
    var i: usize = 0;
    while (i < pad) : (i += 1)
        out[i] = '0'; // zero-pad up to the minimum
    var j: usize = 0;
    while (j < nd) : (j += 1) {
        out[pad + j] = tmp[nd - 1 - j]; // emit MSB-first
    }
    return out;
}

/// Apply a width-bit operation. `a` is the (unary) operand for `.@"not"`;
/// `b` is the second operand for binary ops and the shift/rotate count for
/// shl/shr/rol/ror. Both operands are normalized to width-bit two's
/// complement first; the result is masked to `width` bits.
///
/// For shl/shr we short-circuit when the shift count meets or exceeds the
/// width: every significant bit is shifted out, so the masked result is zero
/// (this also keeps every shift amount below 128 — Zig left-shifts by >= the
/// type's bit count are illegal behavior).
pub fn bitwise(op: Op, a: i128, b: i128, width: Width) u128 {
    const m = mask(width);
    const x = normalize(a, width);
    const y = normalize(b, width);
    const w: u128 = width;

    return switch (op) {
        .@"and" => x & y,
        .@"or" => x | y,
        .xor => x ^ y,
        .@"not" => ~x & m,
        .shl => if (y >= w) 0 else (x << @intCast(y)) & m,
        // x is normalized non-negative → logical (zero-filling) shift.
        .shr => if (y >= w) 0 else x >> @intCast(y),
        .rol, .ror => blk: {
            const shift = y % w; // rotate amount wraps within width
            if (shift == 0)
                break :blk x;
            // A right-rotate by `shift` is a left-rotate by (width - shift).
            const s = if (op == .rol) shift else w - shift;
            break :blk ((x << @intCast(s)) | (x >> @intCast(w - s))) & m;
        },
    };
}

/// Fixed-width binary string of `width` bits (MSB first), freshly allocated.
/// Formats the unsigned normalized pattern directly: a width-64 pattern can
/// exceed i64 range and must not round-trip through the signed path.
pub fn to_bits(alloc: std.mem.Allocator, n: i128, width: Width) std.mem.Allocator.Error![]u8 {
    return format(alloc, @intCast(normalize(n, width)), .bin, width);
}

/// Indices of set bits (LSB = index 0), normalized to `width`, in ascending
/// order. The caller owns the returned slice. Stops at the highest set bit.
pub fn flags(alloc: std.mem.Allocator, n: i128, width: Width) std.mem.Allocator.Error![]u32 {
    var v = normalize(n, width);
    var tmp: [64]u32 = undefined; // at most `width` (<= 64) indices
    var count: usize = 0;
    var i: u32 = 0;
    while (v != 0) {
        if (v & 1 != 0) {
            tmp[count] = i;
            count += 1;
        }
        v >>= 1;
        i += 1;
    }
    const out = try alloc.alloc(u32, count);
    @memcpy(out, tmp[0..count]);
    return out;
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →