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 →