aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2024-07-12 23:52:47 +0900
committernsfisis <nsfisis@gmail.com>2024-07-13 13:15:59 +0900
commit1f4170811730477e9cd7d9620608c4ab619bdefc (patch)
tree32a8517bd8f15dd8bcfb8b38254dbca063090b1a
parent6a6d911dfd02c0db94e2593b3d1d4d9afff77204 (diff)
downloadphp-waddiwasi-1f4170811730477e9cd7d9620608c4ab619bdefc.tar.gz
php-waddiwasi-1f4170811730477e9cd7d9620608c4ab619bdefc.tar.zst
php-waddiwasi-1f4170811730477e9cd7d9620608c4ab619bdefc.zip
feat: partially implement NaN propagation
-rw-r--r--src/WebAssembly/Execution/NumericOps.php68
-rw-r--r--tests/src/SpecTestsuites/SpecTestsuiteBase.php55
2 files changed, 112 insertions, 11 deletions
diff --git a/src/WebAssembly/Execution/NumericOps.php b/src/WebAssembly/Execution/NumericOps.php
index b5e8cb1..70d52b5 100644
--- a/src/WebAssembly/Execution/NumericOps.php
+++ b/src/WebAssembly/Execution/NumericOps.php
@@ -50,6 +50,9 @@ final readonly class NumericOps
public static function f32Ceil(float $x): float
{
+ if (is_nan($x)) {
+ return NAN;
+ }
return ceil($x);
}
@@ -78,7 +81,7 @@ final readonly class NumericOps
{
$xSign = self::getFloatSign($x);
$ySign = self::getFloatSign($y);
- return $xSign === $ySign ? $x : -$x;
+ return $xSign === $ySign ? $x : self::f32Neg($x);
}
public static function f32DemoteF64(float $x): float
@@ -98,6 +101,9 @@ final readonly class NumericOps
public static function f32Floor(float $x): float
{
+ if (is_nan($x)) {
+ return NAN;
+ }
return floor($x);
}
@@ -151,12 +157,20 @@ final readonly class NumericOps
public static function f32Nearest(float $x): float
{
+ if (is_nan($x)) {
+ return NAN;
+ }
return round($x, mode: PHP_ROUND_HALF_EVEN);
}
public static function f32Neg(float $x): float
{
- return -$x;
+ if (is_nan($x)) {
+ // Negate operator does not work for NaN in PHP.
+ return self::constructNan(-self::getFloatSign($x), $x);
+ } else {
+ return -$x;
+ }
}
public static function f32ReinterpretI32(int $x): float
@@ -181,6 +195,9 @@ final readonly class NumericOps
public static function f32Trunc(float $x): float
{
+ if (is_nan($x)) {
+ return NAN;
+ }
if ($x < 0) {
return self::truncateF64ToF32(ceil($x));
} else {
@@ -200,6 +217,9 @@ final readonly class NumericOps
public static function f64Ceil(float $x): float
{
+ if (is_nan($x)) {
+ return NAN;
+ }
return ceil($x);
}
@@ -227,7 +247,7 @@ final readonly class NumericOps
{
$xSign = self::getFloatSign($x);
$ySign = self::getFloatSign($y);
- return $xSign === $ySign ? $x : -$x;
+ return $xSign === $ySign ? $x : self::f64Neg($x);
}
public static function f64Div(float $x, float $y): float
@@ -242,6 +262,9 @@ final readonly class NumericOps
public static function f64Floor(float $x): float
{
+ if (is_nan($x)) {
+ return NAN;
+ }
return floor($x);
}
@@ -295,12 +318,20 @@ final readonly class NumericOps
public static function f64Nearest(float $x): float
{
+ if (is_nan($x)) {
+ return NAN;
+ }
return round($x, mode: PHP_ROUND_HALF_EVEN);
}
public static function f64Neg(float $x): float
{
- return -$x;
+ if (is_nan($x)) {
+ // Negate operator does not work for NaN in PHP.
+ return self::constructNan(-self::getFloatSign($x), $x);
+ } else {
+ return -$x;
+ }
}
public static function f64PromoteF32(float $x): float
@@ -330,6 +361,9 @@ final readonly class NumericOps
public static function f64Trunc(float $x): float
{
+ if (is_nan($x)) {
+ return NAN;
+ }
if ($x < 0) {
return ceil($x);
} else {
@@ -1232,6 +1266,9 @@ final readonly class NumericOps
return (int)$result;
}
+ /**
+ * @return 1|-1
+ */
public static function getFloatSign(float $p): int
{
if (is_nan($p)) {
@@ -1245,4 +1282,27 @@ final readonly class NumericOps
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,
+ ];
+ }
}
diff --git a/tests/src/SpecTestsuites/SpecTestsuiteBase.php b/tests/src/SpecTestsuites/SpecTestsuiteBase.php
index 9b7f24e..341ca4b 100644
--- a/tests/src/SpecTestsuites/SpecTestsuiteBase.php
+++ b/tests/src/SpecTestsuites/SpecTestsuiteBase.php
@@ -12,6 +12,7 @@ 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;
@@ -247,7 +248,7 @@ abstract class SpecTestsuiteBase extends TestCase
/**
* @param array{type: string, value: string} $arg
*/
- private static function wastValueToInternalValue(array $arg): int|float|Ref
+ private static function wastValueToInternalValue(array $arg): int|string|float|Ref
{
$type = $arg['type'];
$value = $arg['value'];
@@ -255,13 +256,11 @@ abstract class SpecTestsuiteBase extends TestCase
'i32' => unpack('l', pack('V', (int)$value))[1],
'i64' => unpack('q', self::convertInt64ToBinary($value))[1],
'f32' => match ($value) {
- 'nan:canonical' => NAN,
- 'nan:arithmetic' => NAN,
+ 'nan:canonical', 'nan:arithmetic' => $value,
default => unpack('g', pack('V', (int)$value))[1],
},
'f64' => match ($value) {
- 'nan:canonical' => NAN,
- 'nan:arithmetic' => NAN,
+ 'nan:canonical', 'nan:arithmetic' => $value,
default => unpack('e', self::convertInt64ToBinary($value))[1],
},
'externref' => $value === 'null' ? Ref::RefNull(ValType::ExternRef) : Ref::RefExtern((int)$value),
@@ -315,12 +314,54 @@ abstract class SpecTestsuiteBase extends TestCase
$expectedResult = $expectedResults[$i];
$expectedValue = self::wastValueToInternalValue($expectedResult);
$actualResult = $actualResults[$i];
- if (is_float($expectedValue) && is_nan($expectedValue)) {
- // @todo check NaN bit pattern.
+ if ($expectedValue === 'nan:canonical') {
$this->assertTrue(
is_nan($actualResult),
"result $i is not NaN" . $message,
);
+ $actualBits = sprintf("%064b", NumericOps::reinterpretF64AsI64($actualResult));
+ if (str_starts_with($actualBits, '0')) {
+ $this->assertSame(
+ sprintf("%064b", NumericOps::reinterpretF64AsI64(NAN)),
+ $actualBits,
+ "result $i is not canonical NaN" . $message,
+ );
+ } else {
+ $this->assertSame(
+ sprintf("1%b", NumericOps::reinterpretF64AsI64(NAN)),
+ $actualBits,
+ "result $i is not canonical NaN" . $message,
+ );
+ }
+ } elseif ($expectedValue === 'nan:arithmetic') {
+ $this->assertTrue(
+ is_nan($actualResult),
+ "result $i is not NaN" . $message,
+ );
+ $actualBits = sprintf("%064b", NumericOps::reinterpretF64AsI64($actualResult));
+ if (str_starts_with($actualBits, '0')) {
+ $this->assertStringStartsWith(
+ '0111111111111',
+ $actualBits,
+ "result $i is not arithmetic NaN" . $message,
+ );
+ } else {
+ $this->assertStringStartsWith(
+ '1111111111111',
+ $actualBits,
+ "result $i is not arithmetic NaN" . $message,
+ );
+ }
+ } elseif (is_float($expectedValue) && is_nan($expectedValue)) {
+ $this->assertTrue(
+ is_nan($actualResult),
+ "result $i is not NaN" . $message,
+ );
+ $this->assertSame(
+ sprintf("%b", NumericOps::reinterpretF64AsI64($expectedValue)),
+ sprintf("%b", NumericOps::reinterpretF64AsI64($actualResult)),
+ "result $i Nan payload mismatch" . $message,
+ );
} elseif ($expectedValue instanceof RefNull) {
$this->assertInstanceOf(
RefNull::class,