Skip to content

Bitwise Calculator — PHP 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 PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.

<?php
// =============================================================================
//  bitwise.php — CosmoDev polyglot showcase port of the `bitwise` tool
// -----------------------------------------------------------------------------
//  Language : PHP 8.0+
//  Source   : ported from src/lib/bitwise.ts (the canonical, live TypeScript lib)
//  License  : display source — part of CosmoDev's polyglot tool pages
//  (dev.cosmolabs.org). Shown verbatim alongside the JS/TS/Go/Rust/Python ports.
// -----------------------------------------------------------------------------
//  Pure, deterministic bitwise calculator. Zero Composer dependencies. Requires
//  the GMP extension (PHP's standard arbitrary-precision integer library,
//  bundled with most PHP distributions). Results are exact across all supported
//  widths (8/16/32/64-bit). 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 GMP is always the unsigned bit-pattern of the
//  width-bit result.
// =============================================================================

declare(strict_types=1);

namespace CosmoDev\Bitwise;

use GMP;
use InvalidArgumentException;

// Radix per base, consumed by gmp_strval and the manual digit parser.
const RADIX = ['bin' => 2, 'oct' => 8, 'dec' => 10, 'hex' => 16];

// Lowercase digit alphabet valid for each base — drives validation and the
// per-character digit-value lookup.
const DIGITS = [
    'bin' => '01',
    'oct' => '01234567',
    'dec' => '0123456789',
    'hex' => '0123456789abcdef',
];

/**
 * Parse a numeric string in $base into a GMP integer.
 *
 * Accepts an optional leading sign and a single optional base prefix
 * (0x/0b/0o, case-insensitive). Throws InvalidArgumentException on empty input
 * or any out-of-base digit. The raw signed value is returned (no width
 * normalization); callers fold it into a field via normalize()/bitwise().
 */
function parse(string $value, string $base): GMP
{
    $trimmed = trim($value);
    if ($trimmed === '' || $trimmed === '-') {
        throw new InvalidArgumentException("Empty {$base} value");
    }

    // Peel off an optional leading '-' so negative literals parse correctly.
    $negative = false;
    $body = $trimmed;
    if ($body[0] === '-') {
        $negative = true;
        $body = substr($body, 1);
    }

    // Strip each base prefix in turn (0x, then 0b, then 0o), case-insensitive —
    // mirrors the reference's chained leading-prefix removal.
    foreach (['0x', '0b', '0o'] as $prefix) {
        if (strncasecmp($body, $prefix, strlen($prefix)) === 0) {
            $body = substr($body, strlen($prefix));
        }
    }

    if ($body === '') {
        throw new InvalidArgumentException("Empty {$base} value");
    }

    // Horner's method: acc = acc*radix + digit, accumulated exactly in GMP.
    $alphabet = DIGITS[$base];
    $radix = RADIX[$base];
    $acc = gmp_init(0);
    foreach (str_split(strtolower($body)) as $ch) {
        $digit = strpos($alphabet, $ch);
        if ($digit === false) {
            throw new InvalidArgumentException("Invalid digit '{$ch}' for base {$base}");
        }
        $acc = gmp_add(gmp_mul($acc, $radix), $digit);
    }

    return $negative ? gmp_neg($acc) : $acc;
}

/**
 * Reduce $n to its unsigned $width-bit two's-complement value.
 *
 * The double-modulo ((n mod m) + m) mod m maps negative dividends into the
 * canonical unsigned range [0, 2^width) — e.g. -1 at width 8 yields 255. (GMP's
 * gmp_mod is already non-negative for a positive divisor, but the double form is
 * kept for parity with the reference algorithm and the other language ports.)
 */
function normalize(GMP|int|string $n, int $width): GMP
{
    $m = gmp_pow(2, $width);                          // 2^width
    return gmp_mod(gmp_add(gmp_mod($n, $m), $m), $m); // canonical [0, 2^width)
}

/**
 * Render $n in $base, zero-padded to at least $minDigits places.
 *
 * Negative values carry a leading '-' and format their magnitude. $minDigits
 * corresponds to the `width` parameter in the TypeScript reference (a width-bit
 * binary value needs exactly $width digits).
 */
function format(GMP|int|string $n, string $base, int $minDigits): string
{
    if (gmp_sign($n) < 0) {
        return '-' . format(gmp_neg($n), $base, $minDigits);
    }
    $radix = RADIX[$base];
    // gmp_strval renders in the requested base; lowercase to match the reference.
    $digits = strtolower(gmp_strval($n, $radix));
    return str_pad($digits, $minDigits, '0', STR_PAD_LEFT);
}

/**
 * Apply a $width-bit bitwise operation.
 *
 * For 'not', $a is the unary operand; for the binary ops, $b is the second
 * operand; for the shift/rotate ops, $b is the count. Both operands are
 * normalized to $width bits first, and the result is masked back into range.
 */
function bitwise(string $op, GMP|int|string $a, GMP|int|string $b, int $width): GMP
{
    $m = gmp_sub(gmp_pow(2, $width), 1); // field mask: 2^width - 1
    $x = normalize($a, $width);
    $y = normalize($b, $width);

    switch ($op) {
        case 'and':
            return gmp_and($x, $y);
        case 'or':
            return gmp_or($x, $y);
        case 'xor':
            return gmp_xor($x, $y);
        case 'not':
            // gmp_com is unbounded; AND the mask to keep the field width.
            return gmp_and(gmp_com($x), $m);
        case 'shl':
            // Left shift grows; for shift >= width the masked result is 0.
            // Short-circuit to avoid a (potentially huge) intermediate.
            if (gmp_cmp($y, $width) >= 0) {
                return gmp_init(0);
            }
            $shift = gmp_intval($y); // y < width <= 64 → safe as a native int
            return gmp_and(gmp_mul($x, gmp_pow(2, $shift)), $m);
        case 'shr':
            // x is normalized non-negative → integer division = logical shift.
            if (gmp_cmp($y, $width) >= 0) {
                return gmp_init(0);
            }
            $shift = gmp_intval($y);
            return gmp_div($x, gmp_pow(2, $shift));
        case 'rol':
        case 'ror':
            $shift = gmp_mod($y, $width);         // rotate amount wraps within width
            if (gmp_cmp($shift, 0) === 0) {
                return $x;
            }
            // A right-rotate by k is a left-rotate by (width - k).
            $s = ($op === 'rol') ? $shift : gmp_sub($width, $shift);
            $sInt = gmp_intval($s);               // s ∈ [1, width-1] → native int
            $left = gmp_mul($x, gmp_pow(2, $sInt));
            $right = gmp_div($x, gmp_pow(2, $width - $sInt));
            return gmp_and(gmp_or($left, $right), $m);
    }
    throw new InvalidArgumentException("Unknown bitwise operation: {$op}");
}

/**
 * Fixed-width binary string of $width bits (MSB first).
 */
function to_bits(GMP|int|string $n, int $width): string
{
    return format(normalize($n, $width), 'bin', $width);
}

/**
 * Indices of set bits in the normalized $width-bit pattern (LSB = index 0).
 * Stops at the highest set bit.
 *
 * @return int[]
 */
function flags(GMP|int|string $n, int $width): array
{
    $bits = normalize($n, $width);
    $out = [];
    $i = 0;
    while (gmp_cmp($bits, 0) > 0) {
        if (gmp_cmp(gmp_and($bits, 1), 0) !== 0) {
            $out[] = $i;
        }
        $bits = gmp_div($bits, 2);
        $i++;
    }
    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 →