diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-04-05 23:26:56 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-04-06 02:03:32 +0900 |
| commit | b47f6c38389762aff3b1b86c179517d3e9d9eb87 (patch) | |
| tree | ff1a3cae32e91a816537fc8dee961a9d4a2d9415 /src/BitOps | |
| parent | 3d0f0b06ee25571034a13a43d2ca660ef687afa9 (diff) | |
| download | php-waddiwasi-b47f6c38389762aff3b1b86c179517d3e9d9eb87.tar.gz php-waddiwasi-b47f6c38389762aff3b1b86c179517d3e9d9eb87.tar.zst php-waddiwasi-b47f6c38389762aff3b1b86c179517d3e9d9eb87.zip | |
refactor: add BitOps component
Diffstat (limited to 'src/BitOps')
| -rw-r--r-- | src/BitOps/BinaryConversion.php | 256 | ||||
| -rw-r--r-- | src/BitOps/FloatOps.php | 40 | ||||
| -rw-r--r-- | src/BitOps/FloatTraits.php | 50 | ||||
| -rw-r--r-- | src/BitOps/PackFloatSpecifiers.php | 11 | ||||
| -rw-r--r-- | src/BitOps/PackIntSpecifiers.php | 14 | ||||
| -rw-r--r-- | src/BitOps/Signedness.php | 24 | ||||
| -rw-r--r-- | src/BitOps/UnpackFloatSpecifiers.php | 19 | ||||
| -rw-r--r-- | src/BitOps/UnpackIntSpecifiers.php | 24 |
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, + }; + } +} |
