aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/BitOps
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-04-06 02:23:01 +0900
committernsfisis <nsfisis@gmail.com>2025-04-06 02:23:01 +0900
commitfa9ad79209d85b0677b00ca1d41d070105fec09f (patch)
tree95a81c0909e761e9cecb3cd7333201cb4ac62161 /src/BitOps
parenta0f17ee6807f9a0605261a11a8ba46c57a9849a0 (diff)
parentde116dceae7ea654df28caab3fd2f3aefdffe188 (diff)
downloadphp-waddiwasi-fa9ad79209d85b0677b00ca1d41d070105fec09f.tar.gz
php-waddiwasi-fa9ad79209d85b0677b00ca1d41d070105fec09f.tar.zst
php-waddiwasi-fa9ad79209d85b0677b00ca1d41d070105fec09f.zip
Merge branch 'fix/float-handling'
Diffstat (limited to 'src/BitOps')
-rw-r--r--src/BitOps/BinaryConversion.php256
-rw-r--r--src/BitOps/FloatOps.php40
-rw-r--r--src/BitOps/FloatTraits.php50
-rw-r--r--src/BitOps/PackFloatSpecifiers.php11
-rw-r--r--src/BitOps/PackIntSpecifiers.php14
-rw-r--r--src/BitOps/Signedness.php24
-rw-r--r--src/BitOps/UnpackFloatSpecifiers.php19
-rw-r--r--src/BitOps/UnpackIntSpecifiers.php24
8 files changed, 438 insertions, 0 deletions
diff --git a/src/BitOps/BinaryConversion.php b/src/BitOps/BinaryConversion.php
new file mode 100644
index 0000000..14b33c9
--- /dev/null
+++ b/src/BitOps/BinaryConversion.php
@@ -0,0 +1,256 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Waddiwasi\BitOps;
+
+use function assert;
+use function is_float;
+use function is_int;
+use function ord;
+use function pack;
+use function strlen;
+use function unpack;
+
+final readonly class BinaryConversion
+{
+ private function __construct()
+ {
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ public static function serializeI8(int $x): string
+ {
+ return self::packInt(PackIntSpecifiers::Int8, $x);
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ public static function serializeI16(int $x): string
+ {
+ return self::packInt(PackIntSpecifiers::Int16LittleEndian, $x);
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ public static function serializeI32(int $x): string
+ {
+ return self::packInt(PackIntSpecifiers::Int32LittleEndian, $x);
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ public static function serializeI64(int $x): string
+ {
+ return self::packInt(PackIntSpecifiers::Int64LittleEndian, $x);
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ public static function serializeI64InBigEndian(int $x): string
+ {
+ return self::packInt(PackIntSpecifiers::Int64BigEndian, $x);
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ public static function serializeF32(float $x): string
+ {
+ // PHP's pack() does not preserve NaN payload bits, so we have to
+ // manually check if the float is NaN and convert it to a 32-bit float.
+ if (is_nan($x)) {
+ [$sign, , $payload] = FloatOps::destructF64Bits($x);
+ $i = 0
+ | FloatTraits::getF32SignBit(Signedness::fromSignBit($sign))
+ | FloatTraits::F32_EXPONENT_NAN
+ | ($payload >> (FloatTraits::F64_MANTISSA_BITS - FloatTraits::F32_MANTISSA_BITS));
+ return self::packInt(PackIntSpecifiers::Int32LittleEndian, $i);
+ } else {
+ return self::packFloat(PackFloatSpecifiers::Float32LittleEndian, $x);
+ }
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ public static function serializeF64(float $x): string
+ {
+ return self::packFloat(PackFloatSpecifiers::Float64LittleEndian, $x);
+ }
+
+ /**
+ * @param non-empty-string $s
+ */
+ public static function deserializeS8(string $s): int
+ {
+ return self::unpackInt(UnpackIntSpecifiers::SignedInt8, $s);
+ }
+
+ /**
+ * @param non-empty-string $s
+ */
+ public static function deserializeS16(string $s): int
+ {
+ // PHP does not support unpacking signed integer in fixed endian, so we
+ // have to swap byte order if the machine's endianness is not little
+ // endian.
+ return self::unpackInt(UnpackIntSpecifiers::SignedInt16MachineOrder, self::byteSwapIfNeeded($s));
+ }
+
+ /**
+ * @param non-empty-string $s
+ */
+ public static function deserializeS32(string $s): int
+ {
+ // PHP does not support unpacking signed integer in fixed endian, so we
+ // have to swap byte order if the machine's endianness is not little
+ // endian.
+ return self::unpackInt(UnpackIntSpecifiers::SignedInt32MachineOrder, self::byteSwapIfNeeded($s));
+ }
+
+ /**
+ * @param non-empty-string $s
+ */
+ public static function deserializeS64(string $s): int
+ {
+ // PHP does not support unpacking signed integer in fixed endian, so we
+ // have to swap byte order if the machine's endianness is not little
+ // endian.
+ return self::unpackInt(UnpackIntSpecifiers::SignedInt64MachineOrder, self::byteSwapIfNeeded($s));
+ }
+
+ /**
+ * @param non-empty-string $s
+ */
+ public static function deserializeF32(string $s): float
+ {
+ // PHP's unpack() does not preserve NaN payload bits, so we have to
+ // manually check if the float is NaN and convert it to a 32-bit float.
+ $i = self::unpackInt(UnpackIntSpecifiers::UnsignedInt32LittleEndian, $s);
+ if (($i & FloatTraits::F32_EXPONENT_MASK) === FloatTraits::F32_EXPONENT_NAN) {
+ $sign = $i & FloatTraits::F32_SIGN_MASK;
+ $payload = $i & FloatTraits::F32_MANTISSA_MASK;
+ $j = 0 |
+ FloatTraits::getF64SignBit(Signedness::fromSignBit($sign)) |
+ FloatTraits::F64_EXPONENT_NAN |
+ ($payload << (FloatTraits::F64_MANTISSA_BITS - FloatTraits::F32_MANTISSA_BITS));
+ return self::unpackFloat(UnpackFloatSpecifiers::Float64LittleEndian, self::packInt(PackIntSpecifiers::Int64LittleEndian, $j));
+ } else {
+ return self::unpackFloat(UnpackFloatSpecifiers::Float32LittleEndian, $s);
+ }
+ }
+
+ /**
+ * @param non-empty-string $s
+ */
+ public static function deserializeF64(string $s): float
+ {
+ return self::unpackFloat(UnpackFloatSpecifiers::Float64LittleEndian, $s);
+ }
+
+ public static function reinterpretI32AsF32(int $x): float
+ {
+ return self::deserializeF32(self::serializeI32($x));
+ }
+
+ public static function reinterpretI64AsF32(int $x): float
+ {
+ return self::deserializeF32(self::serializeI64($x));
+ }
+
+ public static function reinterpretI32AsF64(int $x): float
+ {
+ return self::deserializeF64(self::serializeI32($x));
+ }
+
+ public static function reinterpretI64AsF64(int $x): float
+ {
+ return self::deserializeF64(self::serializeI64($x));
+ }
+
+ public static function reinterpretF32AsI32(float $x): int
+ {
+ return self::deserializeS32(self::serializeF32($x));
+ }
+
+ public static function reinterpretF64AsI32(float $x): int
+ {
+ return self::deserializeS32(self::serializeF64($x));
+ }
+
+ public static function reinterpretF32AsI64(float $x): int
+ {
+ return self::deserializeS64(self::serializeF32($x));
+ }
+
+ public static function reinterpretF64AsI64(float $x): int
+ {
+ return self::deserializeS64(self::serializeF64($x));
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ private static function packInt(PackIntSpecifiers $spec, int $x): string
+ {
+ $result = pack($spec->value, $x);
+ assert($result !== '');
+ return $result;
+ }
+
+ /**
+ * @return non-empty-string
+ */
+ private static function packFloat(PackFloatSpecifiers $spec, float $x): string
+ {
+ $result = pack($spec->value, $x);
+ assert($result !== '');
+ return $result;
+ }
+
+ /**
+ * @param non-empty-string $s
+ */
+ private static function unpackInt(UnpackIntSpecifiers $spec, string $s): int
+ {
+ assert(strlen($s) === $spec->byteCount());
+ $result = unpack($spec->value, $s);
+ assert($result !== false && isset($result[1]) && is_int($result[1]));
+ return $result[1];
+ }
+
+ /**
+ * @param non-empty-string $s
+ */
+ private static function unpackFloat(UnpackFloatSpecifiers $spec, string $s): float
+ {
+ assert(strlen($s) === $spec->byteCount());
+ $result = unpack($spec->value, $s);
+ assert($result !== false && isset($result[1]) && is_float($result[1]));
+ return $result[1];
+ }
+
+ private static function isLittleEndian(): bool
+ {
+ return pack("s", ord("a"))[0] === "a";
+ }
+
+ /**
+ * @param non-empty-string $s
+ * @return non-empty-string
+ */
+ private static function byteSwapIfNeeded(string $s): string
+ {
+ // note: currently phpstan cannot infer that strrev(non-empty-string) returns non-empty-string.
+ $ret = self::isLittleEndian() ? $s : strrev($s);
+ assert($ret !== '');
+ return $ret;
+ }
+}
diff --git a/src/BitOps/FloatOps.php b/src/BitOps/FloatOps.php
new file mode 100644
index 0000000..cb6cbc4
--- /dev/null
+++ b/src/BitOps/FloatOps.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Waddiwasi\BitOps;
+
+final readonly class FloatOps
+{
+ private function __construct()
+ {
+ }
+
+ /**
+ * @return array{int, int, int}
+ */
+ public static function destructF64Bits(float $x): array
+ {
+ $i = BinaryConversion::deserializeS64(BinaryConversion::serializeF64($x));
+ return [
+ $i & FloatTraits::F64_SIGN_MASK,
+ $i & FloatTraits::F64_EXPONENT_MASK,
+ $i & FloatTraits::F64_MANTISSA_MASK,
+ ];
+ }
+
+ public static function constructNan(Signedness $sign, float $x): float
+ {
+ [, , $payload] = self::destructF64Bits($x);
+ $i = 0 |
+ FloatTraits::getF64SignBit($sign) |
+ FloatTraits::F64_EXPONENT_NAN |
+ $payload;
+ return BinaryConversion::deserializeF64(BinaryConversion::serializeI64($i));
+ }
+
+ public static function getSignedness(float $x): Signedness
+ {
+ return self::destructF64Bits($x)[0] === 0 ? Signedness::Unsigned : Signedness::Signed;
+ }
+}
diff --git a/src/BitOps/FloatTraits.php b/src/BitOps/FloatTraits.php
new file mode 100644
index 0000000..dca9e18
--- /dev/null
+++ b/src/BitOps/FloatTraits.php
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Waddiwasi\BitOps;
+
+final readonly class FloatTraits
+{
+ public const int F32_EXPONENT_BITS = 8;
+ public const int F32_MANTISSA_BITS = 23;
+
+ public const int F32_SIGN_MASK = 0b10000000_00000000_00000000_00000000;
+ public const int F32_EXPONENT_MASK = 0b01111111_10000000_00000000_00000000;
+ public const int F32_MANTISSA_MASK = 0b00000000_01111111_11111111_11111111;
+
+ public const int F32_SIGN_UNSIGNED = 0;
+ public const int F32_SIGN_SIGNED = 0b10000000_00000000_00000000_00000000;
+ public const int F32_EXPONENT_NAN = 0b01111111_10000000_00000000_00000000;
+
+ public const int F64_EXPONENT_BITS = 11;
+ public const int F64_MANTISSA_BITS = 52;
+
+ public const int F64_SIGN_MASK = PHP_INT_MIN;
+ public const int F64_EXPONENT_MASK = 0b01111111_11110000_00000000_00000000_00000000_00000000_00000000_00000000;
+ public const int F64_MANTISSA_MASK = 0b00000000_00001111_11111111_11111111_11111111_11111111_11111111_11111111;
+
+ public const int F64_SIGN_UNSIGNED = 0;
+ public const int F64_SIGN_SIGNED = PHP_INT_MIN;
+ public const int F64_EXPONENT_NAN = 0b01111111_11110000_00000000_00000000_00000000_00000000_00000000_00000000;
+
+ private function __construct()
+ {
+ }
+
+ public static function getF32SignBit(Signedness $sign): int
+ {
+ return match ($sign) {
+ Signedness::Unsigned => self::F32_SIGN_UNSIGNED,
+ Signedness::Signed => self::F32_SIGN_SIGNED,
+ };
+ }
+
+ public static function getF64SignBit(Signedness $sign): int
+ {
+ return match ($sign) {
+ Signedness::Unsigned => self::F64_SIGN_UNSIGNED,
+ Signedness::Signed => self::F64_SIGN_SIGNED,
+ };
+ }
+}
diff --git a/src/BitOps/PackFloatSpecifiers.php b/src/BitOps/PackFloatSpecifiers.php
new file mode 100644
index 0000000..645cae0
--- /dev/null
+++ b/src/BitOps/PackFloatSpecifiers.php
@@ -0,0 +1,11 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Waddiwasi\BitOps;
+
+enum PackFloatSpecifiers: string
+{
+ case Float32LittleEndian = 'g';
+ case Float64LittleEndian = 'e';
+}
diff --git a/src/BitOps/PackIntSpecifiers.php b/src/BitOps/PackIntSpecifiers.php
new file mode 100644
index 0000000..da10ab2
--- /dev/null
+++ b/src/BitOps/PackIntSpecifiers.php
@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Waddiwasi\BitOps;
+
+enum PackIntSpecifiers: string
+{
+ case Int8 = 'c';
+ case Int16LittleEndian = 'v';
+ case Int32LittleEndian = 'V';
+ case Int64BigEndian = 'J';
+ case Int64LittleEndian = 'P';
+}
diff --git a/src/BitOps/Signedness.php b/src/BitOps/Signedness.php
new file mode 100644
index 0000000..80cd03b
--- /dev/null
+++ b/src/BitOps/Signedness.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Waddiwasi\BitOps;
+
+enum Signedness
+{
+ case Unsigned;
+ case Signed;
+
+ public static function fromSignBit(int $b): self
+ {
+ return $b === 0 ? self::Unsigned : self::Signed;
+ }
+
+ public function negated(): self
+ {
+ return match ($this) {
+ self::Unsigned => self::Signed,
+ self::Signed => self::Unsigned,
+ };
+ }
+}
diff --git a/src/BitOps/UnpackFloatSpecifiers.php b/src/BitOps/UnpackFloatSpecifiers.php
new file mode 100644
index 0000000..5a3aa4f
--- /dev/null
+++ b/src/BitOps/UnpackFloatSpecifiers.php
@@ -0,0 +1,19 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Waddiwasi\BitOps;
+
+enum UnpackFloatSpecifiers: string
+{
+ case Float32LittleEndian = 'g';
+ case Float64LittleEndian = 'e';
+
+ public function byteCount(): int
+ {
+ return match ($this) {
+ self::Float32LittleEndian => 4,
+ self::Float64LittleEndian => 8,
+ };
+ }
+}
diff --git a/src/BitOps/UnpackIntSpecifiers.php b/src/BitOps/UnpackIntSpecifiers.php
new file mode 100644
index 0000000..be210f3
--- /dev/null
+++ b/src/BitOps/UnpackIntSpecifiers.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Nsfisis\Waddiwasi\BitOps;
+
+enum UnpackIntSpecifiers: string
+{
+ case SignedInt8 = 'c';
+ case SignedInt16MachineOrder = 's';
+ case SignedInt32MachineOrder = 'l';
+ case UnsignedInt32LittleEndian = 'V';
+ case SignedInt64MachineOrder = 'q';
+
+ public function byteCount(): int
+ {
+ return match ($this) {
+ self::SignedInt8 => 1,
+ self::SignedInt16MachineOrder => 2,
+ self::SignedInt32MachineOrder, self::UnsignedInt32LittleEndian => 4,
+ self::SignedInt64MachineOrder => 8,
+ };
+ }
+}