decode(); self::$modules[$moduleName] = $module; if (self::$store === null) { self::$store = Store::empty(); } $spectestExterns = [ 'memory' => Extern::Mem(new MemInst(new MemType(new Limits(1, 2)))), 'table' => Extern::Table(new TableInst(new TableType(new Limits(10, 20), ValType::FuncRef), [ Ref::RefNull(ValType::FuncRef), Ref::RefNull(ValType::FuncRef), Ref::RefNull(ValType::FuncRef), Ref::RefNull(ValType::FuncRef), Ref::RefNull(ValType::FuncRef), Ref::RefNull(ValType::FuncRef), Ref::RefNull(ValType::FuncRef), Ref::RefNull(ValType::FuncRef), Ref::RefNull(ValType::FuncRef), Ref::RefNull(ValType::FuncRef), ])), 'global_i32' => Extern::Global_(new GlobalInst(new GlobalType(Mut::Const, ValType::I32), 666)), 'global_i64' => Extern::Global_(new GlobalInst(new GlobalType(Mut::Const, ValType::I64), 666)), 'global_f32' => Extern::Global_(new GlobalInst(new GlobalType(Mut::Const, ValType::F32), NumericOps::truncateF64ToF32(666.6))), 'global_f64' => Extern::Global_(new GlobalInst(new GlobalType(Mut::Const, ValType::F64), 666.6)), 'print' => Extern::Func(FuncInst::Host(new FuncType([], []), fn () => null)), 'print_i32' => Extern::Func(FuncInst::Host(new FuncType([ValType::I32], []), fn () => null)), 'print_i64' => Extern::Func(FuncInst::Host(new FuncType([ValType::I64], []), fn () => null)), 'print_f32' => Extern::Func(FuncInst::Host(new FuncType([ValType::F32], []), fn () => null)), 'print_f64' => Extern::Func(FuncInst::Host(new FuncType([ValType::F64], []), fn () => null)), 'print_i32_f32' => Extern::Func(FuncInst::Host(new FuncType([ValType::I32, ValType::F32], []), fn () => null)), 'print_f64_f64' => Extern::Func(FuncInst::Host(new FuncType([ValType::F64, ValType::F64], []), fn () => null)), ]; $linker = new Linker(self::$store); foreach ($spectestExterns as $externName => $extern) { $linker->register('spectest', $externName, $extern); } foreach (self::$registeredRuntimes as $registeredModuleName => $registeredRuntime) { $linker->registerNamespace($registeredModuleName, $registeredRuntime); } $runtime = Runtime::instantiate($module, $linker); self::$runtimes[$moduleName] = $runtime; if ($moduleName !== '_') { self::$modules['_'] = $module; self::$runtimes['_'] = $runtime; } $this->assertTrue(true); } protected function runAssertReturnCommand( array $action, array $expected, int $line, ): void { try { $this->assertWasmInvokeResults( $expected, $this->doAction($action), "at {$line}", ); } catch (TrapException $e) { $this->assertTrue(false, "assert_return: trap, {$e} at {$line}"); } } protected function runAssertTrapCommand( array $action, string $text, int $line, ): void { $exception = null; try { $this->doAction($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}"; $wasmBinaryStream = new FileStream($filePath); $exception = null; try { (new Decoder($wasmBinaryStream))->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( array $action, string $text, int $line, ): void { $exception = null; try { $this->doAction($action); } catch (StackOverflowException $e) { $exception = $e; } $this->assertNotNull($exception, "at {$line}"); // @todo Check $text? } protected function runAssertUninstantiableCommand( string $filename, string $text, int $line, ): void { $filePath = __DIR__ . "/../../fixtures/spec_testsuites/core/{$filename}"; $wasmBinaryStream = new FileStream($filePath); $module = (new Decoder($wasmBinaryStream))->decode(); $exception = null; if (self::$store === null) { self::$store = Store::empty(); } $linker = new Linker(self::$store); foreach (self::$registeredRuntimes as $registeredModuleName => $registeredRuntime) { $linker->registerNamespace($registeredModuleName, $registeredRuntime); } try { Runtime::instantiate($module, $linker); } catch (RuntimeException $e) { $exception = $e; } // @todo Check error message. $this->assertNotNull($exception, "instantiating {$filename} is expected to fail (at {$line})"); } protected function runAssertUnlinkableCommand( string $filename, string $text, int $line, ): void { $filePath = __DIR__ . "/../../fixtures/spec_testsuites/core/{$filename}"; $wasmBinaryStream = new FileStream($filePath); $module = (new Decoder($wasmBinaryStream))->decode(); $exception = null; if (self::$store === null) { self::$store = Store::empty(); } $linker = new Linker(self::$store); foreach (self::$registeredRuntimes as $registeredModuleName => $registeredRuntime) { $linker->registerNamespace($registeredModuleName, $registeredRuntime); } try { Runtime::instantiate($module, $linker); } catch (RuntimeException $e) { $exception = $e; } // @todo Check error message. $this->assertNotNull($exception, "linking {$filename} is expected to fail (at {$line})"); } protected function runActionCommand( array $action, int $line, ): void { $this->doAction($action); $this->assertTrue(true); } protected function runRegisterCommand( ?string $name, string $as, int $line, ): void { $targetModuleName = $name ?? '_'; $targetModule = self::$modules[$targetModuleName]; $runtime = self::$runtimes[$targetModuleName]; self::$registeredModules[$as] = $targetModule; self::$registeredRuntimes[$as] = $runtime; if ($name !== '_') { self::$registeredModules[$name] = $targetModule; self::$registeredRuntimes[$name] = $runtime; } $this->assertTrue(true); } /** * @param array{type: string, value: string} $arg */ private static function wastValueToInternalValue(array $arg): int|string|float|Ref { $type = $arg['type']; $value = $arg['value']; return match ($type) { 'i32' => BinaryConversion::deserializeS32(BinaryConversion::serializeI32((int) $value)), 'i64' => BinaryConversion::deserializeS64(self::convertInt64ToBinary($value)), 'f32' => match ($value) { 'nan:canonical', 'nan:arithmetic' => $value, default => BinaryConversion::deserializeF32(BinaryConversion::serializeI32((int) $value)), }, 'f64' => match ($value) { 'nan:canonical', 'nan:arithmetic' => $value, 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), default => throw new RuntimeException("unknown type: {$type}"), }; } private function doAction( array $action, ): array { $targetModuleName = $action['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), ); } elseif ($actionType === 'get') { $runtime = self::$runtimes[$targetModuleName]; $addr = $runtime->getExport($actionField)->addr; return [$runtime->store->globals[$addr]->value]; } $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 ($expectedValue === 'nan:canonical') { $this->assertTrue( is_nan($actualResult), "result {$i} is not NaN" . $message, ); $actualBits = sprintf('%064b', BinaryConversion::reinterpretF64AsI64($actualResult)); if (str_starts_with($actualBits, '0')) { $this->assertSame( sprintf('%064b', BinaryConversion::reinterpretF64AsI64(NAN)), $actualBits, "result {$i} is not canonical NaN" . $message, ); } else { $this->assertSame( sprintf('1%b', BinaryConversion::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', BinaryConversion::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', BinaryConversion::reinterpretF64AsI64($expectedValue)), sprintf('%b', BinaryConversion::reinterpretF64AsI64($actualResult)), "result {$i} Nan payload mismatch" . $message, ); } elseif ($expectedValue instanceof RefNull) { $this->assertInstanceOf( RefNull::class, $actualResult, "result {$i} is not a null" . $message, ); } elseif ($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, ); } elseif ($expectedValue instanceof RefFunc) { $this->assertInstanceOf( RefFunc::class, $actualResult, "result {$i} is not an funcref" . $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::Unreachable => 'unreachable', 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', TrapKind::DivideByZero => 'integer divide by zero', TrapKind::IntegerOverflow => 'integer overflow', TrapKind::InvalidConversionToInteger => 'invalid conversion to integer', }; $this->assertStringContainsString( $actualErrorMessage, $expectedErrorMessage, 'trap kind mismatch' . $message, ); } private static 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 BinaryConversion::serializeI64((int) $value); } }