aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-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
-rw-r--r--src/WebAssembly/BinaryFormat/Decoder.php30
-rw-r--r--src/WebAssembly/Execution/NumericOps.php243
-rw-r--r--tests/src/SpecTestsuites/SpecTestsuiteBase.php36
11 files changed, 491 insertions, 256 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,
+ };
+ }
+}
diff --git a/src/WebAssembly/BinaryFormat/Decoder.php b/src/WebAssembly/BinaryFormat/Decoder.php
index cce3c5c..f0a5c29 100644
--- a/src/WebAssembly/BinaryFormat/Decoder.php
+++ b/src/WebAssembly/BinaryFormat/Decoder.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Nsfisis\Waddiwasi\WebAssembly\BinaryFormat;
+use Nsfisis\Waddiwasi\BitOps\BinaryConversion;
use Nsfisis\Waddiwasi\Stream\StreamInterface;
use Nsfisis\Waddiwasi\Stream\UnexpectedEofException;
use Nsfisis\Waddiwasi\WebAssembly\BinaryFormat\Internal\Code;
@@ -39,7 +40,6 @@ use function assert;
use function count;
use function get_class;
use function in_array;
-use function is_float;
use function is_int;
use function ord;
use function sprintf;
@@ -1090,25 +1090,7 @@ final class Decoder
*/
private function decodeF32(): float
{
- $buf = $this->stream->read(4);
- $result = unpack('V', $buf);
- if ($result === false) {
- throw new InvalidBinaryFormatException("f32");
- }
- assert(isset($result[1]) && is_int($result[1]));
- $i = $result[1];
- if (($i & 0b01111111100000000000000000000000) === 0b01111111100000000000000000000000) {
- $sign = ($i & 0b10000000000000000000000000000000) === 0 ? 1 : -1;
- $payload = $i & 0b00000000011111111111111111111111;
- $j = ($sign === 1 ? 0 : PHP_INT_MIN) | 0b0111111111110000000000000000000000000000000000000000000000000000 | ($payload << (52 - 23));
- $result = unpack('d', pack('q', $j));
- assert(isset($result[1]) && is_float($result[1]));
- return $result[1];
- } else {
- $result = unpack('g', $buf);
- assert(isset($result[1]) && is_float($result[1]));
- return $result[1];
- }
+ return BinaryConversion::deserializeF32($this->stream->read(4));
}
/**
@@ -1116,13 +1098,7 @@ final class Decoder
*/
private function decodeF64(): float
{
- $buf = $this->stream->read(8);
- $result = unpack('e', $buf);
- if ($result === false) {
- throw new InvalidBinaryFormatException("f64");
- }
- assert(isset($result[1]) && is_float($result[1]));
- return $result[1];
+ return BinaryConversion::deserializeF64($this->stream->read(8));
}
/**
diff --git a/src/WebAssembly/Execution/NumericOps.php b/src/WebAssembly/Execution/NumericOps.php
index 8c061bd..2b48679 100644
--- a/src/WebAssembly/Execution/NumericOps.php
+++ b/src/WebAssembly/Execution/NumericOps.php
@@ -5,7 +5,8 @@ declare(strict_types=1);
namespace Nsfisis\Waddiwasi\WebAssembly\Execution;
use FFI;
-use InvalidArgumentException;
+use Nsfisis\Waddiwasi\BitOps\BinaryConversion;
+use Nsfisis\Waddiwasi\BitOps\FloatOps;
use RoundingMode;
use function abs;
use function assert;
@@ -25,12 +26,10 @@ use function is_int;
use function is_nan;
use function max;
use function min;
-use function pack;
use function round;
use function sprintf;
use function sqrt;
use function substr;
-use function unpack;
use const PHP_INT_MAX;
use const PHP_INT_MIN;
@@ -82,14 +81,18 @@ final readonly class NumericOps
public static function f32CopySign(float $x, float $y): float
{
- $xSign = self::getFloatSign($x);
- $ySign = self::getFloatSign($y);
+ $xSign = FloatOps::getSignedness($x);
+ $ySign = FloatOps::getSignedness($y);
return $xSign === $ySign ? $x : self::f32Neg($x);
}
public static function f32DemoteF64(float $x): float
{
- return self::truncateF64ToF32($x);
+ if (is_nan($x)) {
+ return NAN;
+ } else {
+ return self::truncateF64ToF32($x);
+ }
}
public static function f32Div(float $x, float $y): float
@@ -168,10 +171,9 @@ final readonly class NumericOps
public static function f32Neg(float $x): float
{
- self::reinterpretF32AsI32($x);
if (is_nan($x)) {
// Negate operator does not work for NaN in PHP.
- return self::constructNan(-self::getFloatSign($x), $x);
+ return FloatOps::constructNan(FloatOps::getSignedness($x)->negated(), $x);
} else {
return -$x;
}
@@ -179,12 +181,12 @@ final readonly class NumericOps
public static function f32ReinterpretI32(int $x): float
{
- return self::reinterpretI32AsF32($x);
+ return BinaryConversion::reinterpretI32AsF32($x);
}
public static function f32ReinterpretI64(int $x): float
{
- return self::reinterpretI64AsF32($x);
+ return BinaryConversion::reinterpretI64AsF32($x);
}
public static function f32Sqrt(float $x): float
@@ -251,8 +253,8 @@ final readonly class NumericOps
public static function f64CopySign(float $x, float $y): float
{
- $xSign = self::getFloatSign($x);
- $ySign = self::getFloatSign($y);
+ $xSign = FloatOps::getSignedness($x);
+ $ySign = FloatOps::getSignedness($y);
return $xSign === $ySign ? $x : self::f64Neg($x);
}
@@ -334,7 +336,7 @@ final readonly class NumericOps
{
if (is_nan($x)) {
// Negate operator does not work for NaN in PHP.
- return self::constructNan(-self::getFloatSign($x), $x);
+ return FloatOps::constructNan(FloatOps::getSignedness($x)->negated(), $x);
} else {
return -$x;
}
@@ -351,12 +353,12 @@ final readonly class NumericOps
public static function f64ReinterpretI32(int $x): float
{
- return self::reinterpretI32AsF64($x);
+ return BinaryConversion::reinterpretI32AsF64($x);
}
public static function f64ReinterpretI64(int $x): float
{
- return self::reinterpretI64AsF64($x);
+ return BinaryConversion::reinterpretI64AsF64($x);
}
public static function f64Sqrt(float $x): float
@@ -455,17 +457,13 @@ final readonly class NumericOps
public static function i32Extend16S(int $x): int
{
$x = self::convertS32ToU32($x);
- $result = unpack('s', pack('S', $x & 0xFFFF));
- assert($result !== false);
- return $result[1];
+ return BinaryConversion::deserializeS16(BinaryConversion::serializeI16($x & 0xFFFF));
}
public static function i32Extend8S(int $x): int
{
$x = self::convertS32ToU32($x);
- $result = unpack('c', pack('C', $x & 0xFF));
- assert($result !== false);
- return $result[1];
+ return BinaryConversion::deserializeS8(BinaryConversion::serializeI8($x & 0xFF));
}
public static function i32GeS(int $x, int $y): bool
@@ -547,12 +545,12 @@ final readonly class NumericOps
public static function i32ReinterpretF32(float $x): int
{
- return self::reinterpretF32AsI32($x);
+ return BinaryConversion::reinterpretF32AsI32($x);
}
public static function i32ReinterpretF64(float $x): int
{
- return self::reinterpretF64AsI32($x);
+ return BinaryConversion::reinterpretF64AsI32($x);
}
public static function i32RemS(int $x, int $y): int|TrapKind
@@ -836,23 +834,17 @@ final readonly class NumericOps
public static function i64Extend16S(int $x): int
{
- $result = unpack('s', pack('S', $x & 0xFFFF));
- assert($result !== false);
- return $result[1];
+ return BinaryConversion::deserializeS16(BinaryConversion::serializeI16($x & 0xFFFF));
}
public static function i64Extend32S(int $x): int
{
- $result = unpack('l', pack('L', $x & 0xFFFFFFFF));
- assert($result !== false);
- return $result[1];
+ return BinaryConversion::deserializeS32(BinaryConversion::serializeI32($x & 0xFFFFFFFF));
}
public static function i64Extend8S(int $x): int
{
- $result = unpack('c', pack('C', $x & 0xFF));
- assert($result !== false);
- return $result[1];
+ return BinaryConversion::deserializeS8(BinaryConversion::serializeI8($x & 0xFF));
}
public static function i64ExtendI32S(int $x): int
@@ -873,8 +865,8 @@ final readonly class NumericOps
public static function i64GeU(int $x, int $y): bool
{
- $yPacked = pack('J', $y);
- $xPacked = pack('J', $x);
+ $yPacked = BinaryConversion::serializeI64InBigEndian($y);
+ $xPacked = BinaryConversion::serializeI64InBigEndian($x);
return $xPacked >= $yPacked;
}
@@ -885,8 +877,8 @@ final readonly class NumericOps
public static function i64GtU(int $x, int $y): bool
{
- $yPacked = pack('J', $y);
- $xPacked = pack('J', $x);
+ $yPacked = BinaryConversion::serializeI64InBigEndian($y);
+ $xPacked = BinaryConversion::serializeI64InBigEndian($x);
return $xPacked > $yPacked;
}
@@ -897,8 +889,8 @@ final readonly class NumericOps
public static function i64LeU(int $x, int $y): bool
{
- $yPacked = pack('J', $y);
- $xPacked = pack('J', $x);
+ $yPacked = BinaryConversion::serializeI64InBigEndian($y);
+ $xPacked = BinaryConversion::serializeI64InBigEndian($x);
return $xPacked <= $yPacked;
}
@@ -909,8 +901,8 @@ final readonly class NumericOps
public static function i64LtU(int $x, int $y): bool
{
- $yPacked = pack('J', $y);
- $xPacked = pack('J', $x);
+ $yPacked = BinaryConversion::serializeI64InBigEndian($y);
+ $xPacked = BinaryConversion::serializeI64InBigEndian($x);
return $xPacked < $yPacked;
}
@@ -942,12 +934,12 @@ final readonly class NumericOps
public static function i64ReinterpretF32(float $x): int
{
- return self::reinterpretF32AsI64($x);
+ return BinaryConversion::reinterpretF32AsI64($x);
}
public static function i64ReinterpretF64(float $x): int
{
- return self::reinterpretF64AsI64($x);
+ return BinaryConversion::reinterpretF64AsI64($x);
}
public static function i64RemS(int $x, int $y): int|TrapKind
@@ -1147,133 +1139,17 @@ final readonly class NumericOps
return $x ^ $y;
}
- public static function reinterpretI32AsF32(int $x): float
- {
- $y = self::convertS32ToU32($x);
- if (($y & 0b01111111100000000000000000000000) === 0b01111111100000000000000000000000) {
- $sign = ($y & 0b10000000000000000000000000000000) === 0 ? 1 : -1;
- $payload = $y & 0b00000000011111111111111111111111;
- $i = ($sign === 1 ? 0 : PHP_INT_MIN) | 0b0111111111110000000000000000000000000000000000000000000000000000 | ($payload << (52 - 23));
- return self::reinterpretI64AsF64($i);
- } else {
- return self::deserializeF32FromBytes(self::serializeI32ToBytes($x));
- }
- }
-
- public static function reinterpretI64AsF32(int $x): float
- {
- return self::deserializeF32FromBytes(self::serializeI64ToBytes($x));
- }
-
- public static function reinterpretI32AsF64(int $x): float
- {
- return self::deserializeF64FromBytes(self::serializeI32ToBytes($x));
- }
-
- public static function reinterpretI64AsF64(int $x): float
- {
- return self::deserializeF64FromBytes(self::serializeI64ToBytes($x));
- }
-
- public static function reinterpretF32AsI32(float $x): int
- {
- if (is_nan($x)) {
- [$sign, , $payload] = self::destructFloat($x);
- $i = 0
- | ($sign === 0 ? 0 : 0b10000000000000000000000000000000)
- | 0b01111111100000000000000000000000
- | ($payload >> (52 - 23));
- return self::convertU32ToS32($i);
- } else {
- return self::deserializeI32FromBytes(self::serializeF32ToBytes($x));
- }
- }
-
- public static function reinterpretF64AsI32(float $x): int
- {
- return self::deserializeI32FromBytes(self::serializeF64ToBytes($x));
- }
-
- public static function reinterpretF32AsI64(float $x): int
- {
- return self::deserializeI64FromBytes(self::serializeF32ToBytes($x));
- }
-
- public static function reinterpretF64AsI64(float $x): int
- {
- return self::deserializeI64FromBytes(self::serializeF64ToBytes($x));
- }
-
public static function truncateF64ToF32(float $x): float
{
- return self::deserializeF32FromBytes(self::serializeF32ToBytes($x));
- }
-
- public static function serializeI32ToBytes(int $x): string
- {
- return pack('l', $x);
- }
-
- public static function deserializeI32FromBytes(string $s): int
- {
- $result = unpack('l', $s);
- if ($result === false) {
- throw new InvalidArgumentException("Failed to unpack i32: $s");
- }
- return $result[1];
- }
-
- public static function serializeI64ToBytes(int $x): string
- {
- return pack('q', $x);
- }
-
- public static function deserializeI64FromBytes(string $s): int
- {
- $result = unpack('q', $s);
- if ($result === false) {
- throw new InvalidArgumentException("Failed to unpack i64: $s");
- }
- return $result[1];
+ return BinaryConversion::deserializeF32(BinaryConversion::serializeF32($x));
}
- public static function serializeF32ToBytes(float $x): string
- {
- return pack('f', $x);
- }
-
- public static function deserializeF32FromBytes(string $s): float
- {
- $result = unpack('f', $s);
- if ($result === false) {
- throw new InvalidArgumentException("Failed to unpack f32: $s");
- }
- return $result[1];
- }
-
- public static function serializeF64ToBytes(float $x): string
- {
- return pack('d', $x);
- }
-
- public static function deserializeF64FromBytes(string $s): float
- {
- $result = unpack('d', $s);
- if ($result === false) {
- throw new InvalidArgumentException("Failed to unpack f64: $s");
- }
- return $result[1];
- }
public static function convertS32ToU32(int $x): int
{
- // assert(-0x80000000 <= $x && $x <= 0x7FFFFFFF, "convertS32ToU32: out of range $x");
+ assert(-0x80000000 <= $x && $x <= 0x7FFFFFFF);
if ($x < 0) {
- $buf = pack('l', $x);
- $result = unpack('L', $buf);
- assert($result !== false);
- assert(0x00000000 <= $result[1] && $result[1] <= 0xFFFFFFFF, "convertS32ToU32: out of range $result[1]");
- return $result[1];
+ return $x + 0x100000000;
} else {
return $x;
}
@@ -1283,11 +1159,7 @@ final readonly class NumericOps
{
assert(0x00000000 <= $x && $x <= 0xFFFFFFFF);
if (($x & 0x80000000) !== 0) {
- $buf = pack('L', $x);
- $result = unpack('l', $buf);
- assert($result !== false);
- assert(-0x80000000 <= $result[1] && $result[1] <= 0x7FFFFFFF, "convertU32ToS32: out of range $result[1]");
- return $result[1];
+ return $x - 0x100000000;
} else {
return $x;
}
@@ -1319,45 +1191,6 @@ final readonly class NumericOps
return (int)$result;
}
- /**
- * @return 1|-1
- */
- public static function getFloatSign(float $p): int
- {
- if (is_nan($p)) {
- $n = self::reinterpretF64AsI64($p);
- // The MSB is the sign bit.
- return (($n >> 63) & 1) === 1 ? -1 : 1;
- } elseif ($p !== 0.0) {
- return $p < 0.0 ? -1 : 1;
- } else {
- // Comparison with 0 does not work for -0.0.
- return fdiv(1, $p) < 0.0 ? -1 : 1;
- }
- }
-
- /**
- * @param -1|1 $sign
- */
- private static function constructNan(int $sign, float $x): float
- {
- [, , $payload] = self::destructFloat($x);
- $i = ($sign === 1 ? 0 : PHP_INT_MIN) | 0b01111111_11110000_00000000_00000000_00000000_00000000_00000000_00000000 | $payload;
- return self::reinterpretI64AsF64($i);
- }
-
- /**
- * @return array{int, int, int}
- */
- private static function destructFloat(float $x): array
- {
- $i = self::reinterpretF64AsI64($x);
- return [
- $i & PHP_INT_MIN,
- $i & 0b01111111_11110000_00000000_00000000_00000000_00000000_00000000_00000000,
- $i & 0b00000000_00001111_11111111_11111111_11111111_11111111_11111111_11111111,
- ];
- }
private static function castBitIntToCFloat(string $x): float
{
diff --git a/tests/src/SpecTestsuites/SpecTestsuiteBase.php b/tests/src/SpecTestsuites/SpecTestsuiteBase.php
index 41afbf8..5b3cc4d 100644
--- a/tests/src/SpecTestsuites/SpecTestsuiteBase.php
+++ b/tests/src/SpecTestsuites/SpecTestsuiteBase.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Nsfisis\Waddiwasi\Tests\SpecTestsuites;
+use Nsfisis\Waddiwasi\BitOps\BinaryConversion;
use Nsfisis\Waddiwasi\Stream\FileStream;
use Nsfisis\Waddiwasi\WebAssembly\BinaryFormat\Decoder;
use Nsfisis\Waddiwasi\WebAssembly\BinaryFormat\InvalidBinaryFormatException;
@@ -12,7 +13,6 @@ use Nsfisis\Waddiwasi\WebAssembly\Execution\FuncInst;
use Nsfisis\Waddiwasi\WebAssembly\Execution\GlobalInst;
use Nsfisis\Waddiwasi\WebAssembly\Execution\Linker;
use Nsfisis\Waddiwasi\WebAssembly\Execution\MemInst;
-use Nsfisis\Waddiwasi\WebAssembly\Execution\NumericOps;
use Nsfisis\Waddiwasi\WebAssembly\Execution\Ref;
use Nsfisis\Waddiwasi\WebAssembly\Execution\Refs\RefExtern;
use Nsfisis\Waddiwasi\WebAssembly\Execution\Refs\RefFunc;
@@ -254,15 +254,15 @@ abstract class SpecTestsuiteBase extends TestCase
$type = $arg['type'];
$value = $arg['value'];
return match ($type) {
- 'i32' => unpack('l', pack('V', (int)$value))[1],
- 'i64' => unpack('q', self::convertInt64ToBinary($value))[1],
+ 'i32' => BinaryConversion::deserializeS32(BinaryConversion::serializeI32((int)$value)),
+ 'i64' => BinaryConversion::deserializeS64(self::convertInt64ToBinary($value)),
'f32' => match ($value) {
'nan:canonical', 'nan:arithmetic' => $value,
- default => self::i32ToF32((int)$value),
+ default => BinaryConversion::deserializeF32(BinaryConversion::serializeI32((int)$value)),
},
'f64' => match ($value) {
'nan:canonical', 'nan:arithmetic' => $value,
- default => unpack('e', self::convertInt64ToBinary($value))[1],
+ default => BinaryConversion::deserializeF64(self::convertInt64ToBinary($value)),
},
'externref' => $value === 'null' ? Ref::RefNull(ValType::ExternRef) : Ref::RefExtern((int)$value),
'funcref' => $value === 'null' ? Ref::RefNull(ValType::FuncRef) : Ref::RefFunc((int)$value),
@@ -270,18 +270,6 @@ abstract class SpecTestsuiteBase extends TestCase
};
}
- private static function i32ToF32(int $x): float
- {
- if (($x & 0b01111111100000000000000000000000) === 0b01111111100000000000000000000000) {
- $sign = ($x & 0b10000000000000000000000000000000) === 0 ? 1 : -1;
- $payload = $x & 0b00000000011111111111111111111111;
- $i = ($sign === 1 ? 0 : PHP_INT_MIN) | 0b0111111111110000000000000000000000000000000000000000000000000000 | ($payload << (52 - 23));
- return unpack('e', pack('q', $i))[1];
- } else {
- return unpack('g', pack('V', $x))[1];
- }
- }
-
private function doAction(
array $action,
): array {
@@ -332,16 +320,16 @@ abstract class SpecTestsuiteBase extends TestCase
is_nan($actualResult),
"result $i is not NaN" . $message,
);
- $actualBits = sprintf("%064b", NumericOps::reinterpretF64AsI64($actualResult));
+ $actualBits = sprintf("%064b", BinaryConversion::reinterpretF64AsI64($actualResult));
if (str_starts_with($actualBits, '0')) {
$this->assertSame(
- sprintf("%064b", NumericOps::reinterpretF64AsI64(NAN)),
+ sprintf("%064b", BinaryConversion::reinterpretF64AsI64(NAN)),
$actualBits,
"result $i is not canonical NaN" . $message,
);
} else {
$this->assertSame(
- sprintf("1%b", NumericOps::reinterpretF64AsI64(NAN)),
+ sprintf("1%b", BinaryConversion::reinterpretF64AsI64(NAN)),
$actualBits,
"result $i is not canonical NaN" . $message,
);
@@ -351,7 +339,7 @@ abstract class SpecTestsuiteBase extends TestCase
is_nan($actualResult),
"result $i is not NaN" . $message,
);
- $actualBits = sprintf("%064b", NumericOps::reinterpretF64AsI64($actualResult));
+ $actualBits = sprintf("%064b", BinaryConversion::reinterpretF64AsI64($actualResult));
if (str_starts_with($actualBits, '0')) {
$this->assertStringStartsWith(
'0111111111111',
@@ -371,8 +359,8 @@ abstract class SpecTestsuiteBase extends TestCase
"result $i is not NaN" . $message,
);
$this->assertSame(
- sprintf("%b", NumericOps::reinterpretF64AsI64($expectedValue)),
- sprintf("%b", NumericOps::reinterpretF64AsI64($actualResult)),
+ sprintf("%b", BinaryConversion::reinterpretF64AsI64($expectedValue)),
+ sprintf("%b", BinaryConversion::reinterpretF64AsI64($actualResult)),
"result $i Nan payload mismatch" . $message,
);
} elseif ($expectedValue instanceof RefNull) {
@@ -446,6 +434,6 @@ abstract class SpecTestsuiteBase extends TestCase
if (bccomp(bcsub(bcpow('2', '63'), '1'), $value) < 0) {
$value = bcsub($value, bcpow('2', '64'));
}
- return pack('q', (int)$value);
+ return BinaryConversion::serializeI64((int)$value);
}
}