decode(); self::$modules[$moduleName] = $module; $runtime = Runtime::instantiate(Store::empty(), $module, []); self::$runtimes[$moduleName] = $runtime; $this->assertTrue(true); } protected function runAssertReturnCommand( ?string $module, array $action, array $expected, int $line, ): void { try { $this->assertWasmInvokeResults( $expected, $this->doAction($module, $action), "at $line", ); } catch (TrapException $e) { $this->assertTrue(false, "assert_return: trap, $e at $line"); } } protected function runAssertTrapCommand( ?string $module, array $action, string $text, int $line, ): void { $exception = null; try { $this->doAction($module, $action); } catch (TrapException $e) { $exception = $e; } $this->assertNotNull($exception, "at $line"); $this->assertTrapKind($text, $e->getTrapKind(), "at $line"); } protected function runAssertMalformedCommand( string $filename, string $text, int $line, ): void { $filePath = __DIR__ . "/../../fixtures/spec_testsuites/core/$filename"; $wasmBinary = file_get_contents($filePath); $exception = null; try { (new Decoder($wasmBinary))->decode(); } catch (InvalidBinaryFormatException $e) { $exception = $e; } // @todo Check error message. $this->assertNotNull($exception, "decoding $filename is expected to fail (at $line)"); } protected function runAssertInvalidCommand( string $filename, string $text, int $line, ): void { // @todo Our implementation does not support "validation" step. $this->assertTrue(true); } protected function runAssertExhaustionCommand( ?string $module, array $action, string $text, int $line, ): void { $exception = null; try { $this->doAction($module, $action); } catch (StackOverflowException $e) { $exception = $e; } $this->assertNotNull($exception, "at $line"); // @todo Check $text? } protected function runAssertUninstantiableCommand( string $filename, string $text, int $line, ): void { $this->assertTrue(false, "assert_uninstantiable"); } protected function runAssertUnlinkableCommand( string $filename, string $text, int $line, ): void { $this->assertTrue(false, "assert_unlinkable"); } protected function runActionCommand( ?string $module, array $action, int $line, ): void { $this->doAction($module, $action); $this->assertTrue(true); } protected function runRegisterCommand( ?string $name, string $as, int $line, ): void { $this->assertTrue(false, "register"); } /** * @param array{type: string, value: string} $arg */ private static function wastValueToInternalValue(array $arg): int|float|RefExtern { $type = $arg['type']; $value = $arg['value']; return match ($type) { 'i32' => unpack('l', pack('l', (int)$value))[1], 'i64' => unpack('q', self::convertInt64ToBinary($value))[1], 'f32' => unpack('g', pack('l', (int)$value))[1], 'f64' => unpack('e', self::convertInt64ToBinary($value))[1], 'externref' => Ref::RefExtern((int)$value), default => throw new \RuntimeException("unknown type: $type"), }; } private function doAction( ?string $module, array $action, ): array { $targetModuleName = $module ?? '_'; $targetModule = self::$modules[$targetModuleName]; $actionType = $action['type']; $actionField = $action['field']; if ($actionType === 'invoke') { $actionArgs = $action['args']; $runtime = self::$runtimes[$targetModuleName]; return $runtime->invoke( $actionField, array_map(self::wastValueToInternalValue(...), $actionArgs), ); } else { $this->assertTrue(false, "unknown action: $actionType"); } } /** * @param list $actualResults * @param list $expectedResults */ private function assertWasmInvokeResults( array $expectedResults, array $actualResults, string $message = '', ): void { if ($message !== '') { $message = " ($message)"; } $this->assertCount( count($expectedResults), $actualResults, 'results count mismatch' . $message, ); for ($i = 0; $i < count($expectedResults); $i++) { $expectedResult = $expectedResults[$i]; $expectedValue = self::wastValueToInternalValue($expectedResult); $actualResult = $actualResults[$i]; if (is_float($expectedValue) && is_nan($expectedValue)) { // @todo check NaN bit pattern. $this->assertTrue( is_nan($actualResult), "result $i is not NaN" . $message, ); } else if ($expectedValue instanceof RefExtern) { $this->assertInstanceOf( RefExtern::class, $actualResult, "result $i is not an externref" . $message, ); $this->assertSame( $expectedValue->addr, $actualResult->addr, "result $i mismatch" . $message, ); } else { $this->assertSame( $expectedValue, $actualResult, "result $i mismatch" . $message, ); } } } private function assertTrapKind( string $expectedErrorMessage, TrapKind $kind, string $message = '', ): void { if ($message !== '') { $message = " ($message)"; } $actualErrorMessage = match ($kind) { TrapKind::Unknown => 'unknown', TrapKind::OutOfBoundsMemoryAccess => 'out of bounds memory access', TrapKind::OutOfBoundsTableAccess => 'out of bounds table access', TrapKind::UninitializedElement => 'uninitialized element', TrapKind::IndirectCallTypeMismatch => 'indirect call type mismatch', TrapKind::UndefinedElement => 'undefined element', }; $this->assertStringContainsString( $actualErrorMessage, $expectedErrorMessage, 'trap kind mismatch' . $message, ); } static private function convertInt64ToBinary(string $value): string { // 2^63-1 < $value if (bccomp(bcsub(bcpow('2', '63'), '1'), $value) < 0) { $value = bcsub($value, bcpow('2', '64')); } return pack('q', (int)$value); } }