diff --git a/.gitignore b/.gitignore index e7284854..ccc31b9a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,6 @@ *.exp *.class *.wasm -/swoole_compiler /tpc tests/**/*.diff tests/**/*.exp @@ -30,3 +29,4 @@ tests/**/*.log tests/**/*.out tests/**/*.php tests/**/*.sh +/browser/node_modules/ diff --git a/phpunit/src/Build/WasiToolchainTest.php b/phpunit/src/Build/WasiToolchainTest.php index b79007a8..0b491b9a 100644 --- a/phpunit/src/Build/WasiToolchainTest.php +++ b/phpunit/src/Build/WasiToolchainTest.php @@ -29,16 +29,18 @@ final class WasiToolchainTest extends TestCase public function testDetectsSupportedToolsOnlyFromPath(): void { - $this->installFakeTools(22, 47, 'wasm32-unknown-wasip1'); + $this->installFakeTools(22, 47, 1, 'wasm32-unknown-wasip2'); putenv('PATH=' . $this->directory); $tools = (new WasiToolchain())->detect(); - $this->assertSame($this->directory . '/clang++', $tools['clang++']); + $this->assertSame($this->directory . '/wasm32-wasip2-clang++', $tools['clang++']); $this->assertSame($this->directory . '/wasmtime', $tools['wasmtime']); - $this->assertSame('wasm32-unknown-wasip1', $tools['target']); + $this->assertSame($this->directory . '/jco', $tools['jco']); + $this->assertSame('wasm32-unknown-wasip2', $tools['target']); $this->assertSame('22.0.0', $tools['clang-version']); $this->assertSame('47.0.0', $tools['wasmtime-version']); + $this->assertSame('1.0.0', $tools['jco-version']); } public function testRejectsMissingTool(): void @@ -46,40 +48,42 @@ final class WasiToolchainTest extends TestCase putenv('PATH=' . $this->directory); $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('`clang` was not found in PATH'); + $this->expectExceptionMessage('`wasm32-wasip2-clang` was not found in PATH'); (new WasiToolchain())->detect(); } public function testRejectsOldLlvm(): void { - $this->installFakeTools(21, 47, 'wasm32-unknown-wasip1'); + $this->installFakeTools(21, 47, 1, 'wasm32-unknown-wasip2'); putenv('PATH=' . $this->directory); $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('`clang` 21 is too old'); + $this->expectExceptionMessage('`wasm32-wasip2-clang` 21 is too old'); (new WasiToolchain())->detect(); } public function testRejectsNonWasiClangTarget(): void { - $this->installFakeTools(22, 47, 'x86_64-unknown-linux-gnu'); + $this->installFakeTools(22, 47, 1, 'x86_64-unknown-linux-gnu'); putenv('PATH=' . $this->directory); $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('not configured for wasm32-wasi'); + $this->expectExceptionMessage('not configured for wasm32-unknown-wasip2'); (new WasiToolchain())->detect(); } - private function installFakeTools(int $llvmMajor, int $wasmtimeMajor, string $target): void + private function installFakeTools(int $llvmMajor, int $wasmtimeMajor, int $jcoMajor, string $target): void { - foreach (['clang', 'llvm-ar', 'llvm-ranlib', 'llvm-nm', 'wasm-ld'] as $tool) { + foreach (['wasm32-wasip2-clang', 'llvm-ar', 'llvm-ranlib', 'llvm-nm'] as $tool) { $this->writeExecutable($tool, "#!/bin/sh\necho 'LLVM version {$llvmMajor}.0.0'\n"); } + $this->writeExecutable('wasm-component-ld', "#!/bin/sh\necho 'wasm-component-ld version 0.5.22'\n"); $this->writeExecutable( - 'clang++', + 'wasm32-wasip2-clang++', "#!/bin/sh\nif [ \"\$1\" = '--print-target-triple' ]; then echo '{$target}'; else echo 'clang version {$llvmMajor}.0.0'; fi\n", ); $this->writeExecutable('wasmtime', "#!/bin/sh\necho 'wasmtime {$wasmtimeMajor}.0.0'\n"); + $this->writeExecutable('jco', "#!/bin/sh\necho 'jco {$jcoMajor}.0.0'\n"); } private function writeExecutable(string $name, string $contents): void diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index 1cdf0567..4fe7b455 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -101,12 +101,12 @@ class CompilerBaseApiTest extends TestCase public function testWasiTargetDetection(): void { - foreach (['wasm32-wasi', 'wasm32-wasip1', 'wasm32-wasip1-threads'] as $target) { + foreach (['wasm32-wasip2', 'wasm32-unknown-wasip2'] as $target) { $this->setPropertyValue('targetPlatform', $target); $this->assertTrue($this->compiler->isWasiTarget(), $target); } - foreach (['', 'wasm32-unknown-unknown', 'aarch64-linux-gnu'] as $target) { + foreach (['', 'wasm32-wasi', 'wasm32-wasip1', 'wasm32-wasip1-threads', 'wasm32-unknown-unknown', 'aarch64-linux-gnu'] as $target) { $this->setPropertyValue('targetPlatform', $target); $this->assertFalse($this->compiler->isWasiTarget(), $target); } diff --git a/phpunit/src/Generator/FiberGeneratorTest.php b/phpunit/src/Generator/FiberGeneratorTest.php index b85efda5..e446c596 100644 --- a/phpunit/src/Generator/FiberGeneratorTest.php +++ b/phpunit/src/Generator/FiberGeneratorTest.php @@ -12,7 +12,7 @@ class FiberGeneratorTest extends TestCase { $compiler = CompilerTest::create(ROOT_PATH); $reflection = new \ReflectionClass($compiler); - $reflection->getProperty('targetPlatform')->setValue($compiler, 'wasm32-wasi'); + $reflection->getProperty('targetPlatform')->setValue($compiler, 'wasm32-wasip2'); $file = __DIR__ . '/../../code/generator-conversion-error.php'; $compiler->addFiles([$file]); diff --git a/phpunit/src/Platform/PlatformTest.php b/phpunit/src/Platform/PlatformTest.php index 031402ee..3833cbfd 100644 --- a/phpunit/src/Platform/PlatformTest.php +++ b/phpunit/src/Platform/PlatformTest.php @@ -12,9 +12,9 @@ class PlatformTest extends TestCase { public function testWasiTargetProperties(): void { - $platform = new Wasi('wasm32-unknown-wasip1'); + $platform = new Wasi('wasm32-unknown-wasip2'); - $this->assertSame('WASI SDK (wasm32-unknown-wasip1)', $platform->getName()); + $this->assertSame('WASI SDK (wasm32-unknown-wasip2)', $platform->getName()); $this->assertSame('.o', $platform->getObjectExtension()); $this->assertSame('.wasm', $platform->getExecutableExtension()); $this->assertSame('.a', $platform->getSharedLibraryExtension()); diff --git a/phpunit/src/WasiUnsupportedSyntaxTest.php b/phpunit/src/WasiUnsupportedSyntaxTest.php index b6142c9a..cea5e6be 100644 --- a/phpunit/src/WasiUnsupportedSyntaxTest.php +++ b/phpunit/src/WasiUnsupportedSyntaxTest.php @@ -12,7 +12,7 @@ class WasiUnsupportedSyntaxTest extends TestCase public function testUnsupportedSyntaxFailsDuringWasiConversion(string $file, string $message): void { $compiler = CompilerTest::create(ROOT_PATH); - (new \ReflectionClass($compiler))->getProperty('targetPlatform')->setValue($compiler, 'wasm32-wasi'); + (new \ReflectionClass($compiler))->getProperty('targetPlatform')->setValue($compiler, 'wasm32-wasip2'); $source = __DIR__ . '/../code/' . $file; $compiler->addFiles([$source]); $compiler->prepareFile($source); @@ -37,6 +37,18 @@ class WasiUnsupportedSyntaxTest extends TestCase 'wasi-generator-arrow.php', 'Fiber and Generator are not supported by the WASI target', ], + 'process function' => [ + 'wasi-process.php', + 'Function `proc_open` is not supported by the WASI target', + ], + 'socket function' => [ + 'wasi-socket.php', + 'Function `stream_socket_server` is not supported by the WASI target', + ], + 'signal function' => [ + 'wasi-signal.php', + 'Function `pcntl_signal` is not supported by the WASI target', + ], ]; } } diff --git a/src/Build/WasiToolchain.php b/src/Build/WasiToolchain.php index e2367e8f..a07da950 100644 --- a/src/Build/WasiToolchain.php +++ b/src/Build/WasiToolchain.php @@ -8,34 +8,50 @@ final class WasiToolchain { public const MIN_LLVM_MAJOR = 22; public const MIN_WASMTIME_MAJOR = 47; + public const MIN_JCO_MAJOR = 1; /** @return array */ public function detect(): array { $tools = []; - foreach (['clang', 'clang++', 'llvm-ar', 'llvm-ranlib', 'llvm-nm', 'wasm-ld', 'wasmtime'] as $name) { + foreach ([ + 'wasm32-wasip2-clang', + 'wasm32-wasip2-clang++', + 'llvm-ar', + 'llvm-ranlib', + 'llvm-nm', + 'wasm-component-ld', + 'wasmtime', + 'jco', + ] as $name) { $tools[$name] = $this->findExecutable($name); } $versions = []; - foreach (['clang', 'clang++', 'llvm-ar', 'llvm-ranlib', 'llvm-nm', 'wasm-ld'] as $name) { + foreach (['wasm32-wasip2-clang', 'wasm32-wasip2-clang++', 'llvm-ar', 'llvm-ranlib', 'llvm-nm'] as $name) { $versions[$name] = $this->requireVersion($name, $tools[$name], self::MIN_LLVM_MAJOR); } + $this->requireVersion('wasm-component-ld', $tools['wasm-component-ld'], 0); $versions['wasmtime'] = $this->requireVersion('wasmtime', $tools['wasmtime'], self::MIN_WASMTIME_MAJOR); + $versions['jco'] = $this->requireVersion('jco', $tools['jco'], self::MIN_JCO_MAJOR); - [$exitCode, $target, $error] = $this->run([$tools['clang++'], '--print-target-triple']); + [$exitCode, $target, $error] = $this->run([$tools['wasm32-wasip2-clang++'], '--print-target-triple']); $target = trim($target); - if ($exitCode !== 0 || preg_match('/^wasm32-(?:unknown-)?wasi(?:p1)?$/', $target) !== 1) { + if ($exitCode !== 0 || $target !== 'wasm32-unknown-wasip2') { $detail = trim($error) !== '' ? ': ' . trim($error) : ''; throw new RuntimeException( - "clang++ from PATH is not configured for wasm32-wasi (reported target: " + "wasm32-wasip2-clang++ from PATH is not configured for wasm32-unknown-wasip2 (reported target: " . ($target !== '' ? $target : 'unknown') . "){$detail}", ); } + $tools['clang'] = $tools['wasm32-wasip2-clang']; + $tools['clang++'] = $tools['wasm32-wasip2-clang++']; + $tools['wasm-ld'] = $tools['wasm-component-ld']; $tools['target'] = $target; - $tools['clang-version'] = $versions['clang++']; + $tools['clang-version'] = $versions['wasm32-wasip2-clang++']; $tools['wasmtime-version'] = $versions['wasmtime']; + $tools['jco-version'] = $versions['jco']; return $tools; } @@ -62,7 +78,7 @@ final class WasiToolchain { [$exitCode, $output, $error] = $this->run([$executable, '--version']); $versionText = trim($output . "\n" . $error); - if ($exitCode !== 0 || preg_match('/(?:version|wasmtime|LLD)\s+((\d+)(?:\.\d+)+)/i', $versionText, $match) !== 1) { + if ($exitCode !== 0 || preg_match('/\bv?((\d+)(?:\.\d+)+)\b/i', $versionText, $match) !== 1) { throw new RuntimeException("Unable to determine the version of WASI tool `{$name}` from PATH"); } $major = (int) $match[2]; diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 7fff7db8..8e84e747 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -174,6 +174,41 @@ class CompilerBase implements PropertyAccessContext 'stream_socket_accept', 'popen', ]; + + /** + * APIs which cannot have the same semantics in Wasmtime and a browser. + * Keep this list at the language boundary so a WASI build never degrades + * into a link error or a browser-only implementation. + */ + private const array WASI_UNSUPPORTED_FUNCTIONS = [ + 'exec', + 'passthru', + 'popen', + 'proc_close', + 'proc_get_status', + 'proc_nice', + 'proc_open', + 'proc_terminate', + 'shell_exec', + 'system', + 'fsockopen', + 'pfsockopen', + 'stream_socket_accept', + 'stream_socket_client', + 'stream_socket_enable_crypto', + 'stream_socket_get_name', + 'stream_socket_pair', + 'stream_socket_recvfrom', + 'stream_socket_sendto', + 'stream_socket_server', + 'stream_socket_shutdown', + ]; + + private const array WASI_UNSUPPORTED_FUNCTION_PREFIXES = [ + 'pcntl_', + 'posix_', + 'socket_', + ]; public const int DECL_TYPE_OF_RETURN = 1; public const int DECL_TYPE_OF_PROPERTY = 2; public const int DECL_TYPE_OF_CONST = 3; @@ -584,7 +619,24 @@ class CompilerBase implements PropertyAccessContext public function isWasiTarget(): bool { $target = strtolower($this->targetPlatform); - return str_starts_with($target, 'wasm32-wasi') || str_starts_with($target, 'wasm32-wasip1'); + return $target === 'wasm32-unknown-wasip2' || $target === 'wasm32-wasip2'; + } + + protected function assertWasiFunctionSupported(NodeAbstract $expr, string $name): void + { + if (!$this->isWasiTarget()) { + return; + } + + $name = strtolower(ltrim($name, '\\')); + if (in_array($name, self::WASI_UNSUPPORTED_FUNCTIONS, true)) { + $this->fatalError($expr, "Function `{$name}` is not supported by the WASI target"); + } + foreach (self::WASI_UNSUPPORTED_FUNCTION_PREFIXES as $prefix) { + if (str_starts_with($name, $prefix)) { + $this->fatalError($expr, "Function `{$name}` is not supported by the WASI target"); + } + } } public function isBuildModeBin(): bool diff --git a/src/Parser/FunctionCallTrait.php b/src/Parser/FunctionCallTrait.php index cc4706f7..48d6f298 100644 --- a/src/Parser/FunctionCallTrait.php +++ b/src/Parser/FunctionCallTrait.php @@ -78,6 +78,9 @@ trait FunctionCallTrait } elseif ($expr->name->getType() === 'Name' or $expr->name->getType() === 'Name_FullyQualified') { $name = $this->parseIdentifier($expr->name); $globalName = ltrim($name, '\\'); + if ($this->isInternalFunction($globalName)) { + $this->assertWasiFunctionSupported($expr, $globalName); + } if (in_array($name, Constants::UNSUPPORTED_FUNCTIONS)) { $this->fatalError($expr, 'Unsupported function: `' . $name . '`'); } diff --git a/src/Platform/Wasi.php b/src/Platform/Wasi.php index 12913555..9765d51b 100644 --- a/src/Platform/Wasi.php +++ b/src/Platform/Wasi.php @@ -4,7 +4,7 @@ namespace TypePhp\Platform; final class Wasi extends UnixPlatform { - public function __construct(private readonly string $target = 'wasm32-wasi') + public function __construct(private readonly string $target = 'wasm32-unknown-wasip2') { } diff --git a/src/Translator.php b/src/Translator.php index f7e12f1b..bc80af2a 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -168,7 +168,10 @@ class Translator extends Preprocessor $targetPlatform = $this->climate->arguments->defined('target-platform') ? (string) $this->climate->arguments->get('target-platform') : ''; - if (str_starts_with($targetPlatform, 'wasm32-wasi') || str_starts_with($targetPlatform, 'wasm32-wasip1')) { + if ($targetPlatform === 'wasm32-wasip1' || $targetPlatform === 'wasm32-wasi') { + throw new \RuntimeException('WASI Preview 1 is not supported; use wasm32-wasip2'); + } + if ($targetPlatform === 'wasm32-wasip2' || $targetPlatform === 'wasm32-unknown-wasip2') { $detectedTarget = getenv('TYPEPHP_WASI_TARGET'); $this->platform = new Wasi( is_string($detectedTarget) && $detectedTarget !== '' ? $detectedTarget : $targetPlatform, diff --git a/src/compiler.php b/src/compiler.php index 87fb0a4a..aa6b2c5d 100644 --- a/src/compiler.php +++ b/src/compiler.php @@ -140,9 +140,11 @@ function compileWasmProgram(array $argv): void $environment['TYPEPHP_WASI_NM'] = $tools['llvm-nm']; $environment['TYPEPHP_WASI_LD'] = $tools['wasm-ld']; $environment['TYPEPHP_WASMTIME'] = $tools['wasmtime']; + $environment['TYPEPHP_JCO'] = $tools['jco']; $environment['TYPEPHP_WASI_TARGET'] = $tools['target']; $environment['TYPEPHP_WASI_CLANG_VERSION'] = $tools['clang-version']; $environment['TYPEPHP_WASMTIME_VERSION'] = $tools['wasmtime-version']; + $environment['TYPEPHP_JCO_VERSION'] = $tools['jco-version']; $environment['TYPEPHP_WASM_PROGRAM_BUILD_DIR'] = $buildDir; $process = proc_open(