From 9f6a13d5f7e43677ae8632c01f07ff2806c142d1 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 10 Jul 2026 18:57:31 +0800 Subject: [PATCH] feat(compiler): add static type checking for composite types - Implement static type validation for union, intersection, and nullable types - Add composite type assignment checking for parameters, return values, and property assignments - Introduce dynamic value wrapper tests to verify type checking behavior - Enhance error messaging for composite type mismatches - Update documentation to reflect improved static type analysis capabilities - Add proper restoration of compiler state after generator conversion errors - Improve parallel compilation process management and error handling - Add metadata caching for object files with PHP ABI compatibility checks --- docs/INCOMPATIBLE_PHP_FEATURES.md | 2 +- phpunit/code/generator-conversion-error.php | 7 + phpunit/src/CompilerBaseApiTest.php | 16 ++ phpunit/src/Generator/FiberGeneratorTest.php | 33 +++ phpunit/src/ParallelCompileTest.php | 107 ++++++++ src/CompilerBase.php | 160 +++++++++++ src/Generator/FiberGenerator.php | 15 ++ src/Translator.php | 252 +++++++++++++----- .../type_decl/intersection-param-check.phpt | 6 +- tests/aot/type_decl/union-param-check.phpt | 12 +- tests/aot/type_hits/009.phpt | 4 +- tests/aot/type_hits/010.phpt | 6 +- 12 files changed, 539 insertions(+), 81 deletions(-) create mode 100644 phpunit/code/generator-conversion-error.php create mode 100644 phpunit/src/Generator/FiberGeneratorTest.php create mode 100644 phpunit/src/ParallelCompileTest.php diff --git a/docs/INCOMPATIBLE_PHP_FEATURES.md b/docs/INCOMPATIBLE_PHP_FEATURES.md index 9d8421aa..43ce564a 100644 --- a/docs/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/INCOMPATIBLE_PHP_FEATURES.md @@ -19,7 +19,7 @@ - `__construct()` 不允许返回值。 - 参数默认值不允许出现在必填参数之前(`PHP`允许,但会直接丢弃此默认参数)。 - 不支持引用可变参数 `&...$args`。 -- 联合类型、交叉类型、`nullable` 类型在静态编译阶段按 `mixed/any` 处理,只保留运行时 type check。 +- 联合类型、交叉类型、`nullable` 类型仍以 `mixed/any` 作为 C++ 表示,但静态阶段会利用已知表达式类型提前拒绝确定不兼容的参数、返回值和属性赋值;动态值仍保留运行时 type check。 - 局部变量类型一旦被静态推断为具体 native 类型,不支持在同一作用域内重新赋值为不兼容类型。 - attribute 参数不支持数组值和 `new` 表达式。 diff --git a/phpunit/code/generator-conversion-error.php b/phpunit/code/generator-conversion-error.php new file mode 100644 index 00000000..1d647acd --- /dev/null +++ b/phpunit/code/generator-conversion-error.php @@ -0,0 +1,7 @@ +assertSame(CompilerBase::BUILD_MODE_BIN, $this->compiler->getBuildMode()); } + public function testMiscObjectCacheIsInvalidatedWhenCompileOptionsChange(): void + { + $source = $this->testDir . '/typephp_runtime.cc'; + $object = $this->testDir . '/typephp_runtime.o'; + file_put_contents($source, "int typephp_runtime_test = 1;\n"); + file_put_contents($object, 'object'); + touch($source, time() - 10); + touch($object, time() + 10); + + $this->invokeMethod('writeMiscObjectCacheMetadata', $source, $object); + $this->assertTrue($this->compiler->hasMiscObjectFileCache($source)); + + $this->setPropertyValue('cxxFlags', '-fno-rtti'); + $this->assertFalse($this->compiler->hasMiscObjectFileCache($source)); + } + // ======================================================================== // getTypeFromZendType // ======================================================================== diff --git a/phpunit/src/Generator/FiberGeneratorTest.php b/phpunit/src/Generator/FiberGeneratorTest.php new file mode 100644 index 00000000..c06a4f52 --- /dev/null +++ b/phpunit/src/Generator/FiberGeneratorTest.php @@ -0,0 +1,33 @@ +addFiles([$file]); + $compiler->prepareFile($file); + + try { + $compiler->convertFile($file); + $this->fail('The variable-variable expression should fail conversion'); + } catch (TestError $e) { + $this->assertStringContainsString('The `$$` syntax is not supported', $e->getMessage()); + } + + $reflection = new \ReflectionClass($compiler); + $this->assertFalse($reflection->getProperty('inGeneratorBody')->getValue($compiler)); + $this->assertSame(0, $reflection->getProperty('indentLevel')->getValue($compiler)); + $this->assertSame('', $reflection->getProperty('function')->getValue($compiler)); + $this->assertNull($reflection->getProperty('functionDef')->getValue($compiler)); + $context = $reflection->getProperty('context')->getValue($compiler); + $this->assertFalse($context->inClosure); + } +} diff --git a/phpunit/src/ParallelCompileTest.php b/phpunit/src/ParallelCompileTest.php new file mode 100644 index 00000000..0ad5c871 --- /dev/null +++ b/phpunit/src/ParallelCompileTest.php @@ -0,0 +1,107 @@ +assertSame([123, 0], $compiler->waitForTest()); + $this->assertSame(2, $compiler->getWaitCallCount()); + } + + public function testWaitFailureOtherThanInterruptionThrows(): void + { + $compiler = new ScriptedWaitCompiler([ + [-1, 0, PCNTL_ECHILD], + ]); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Failed to wait for compiler process'); + $compiler->waitForTest(); + } + + public function testSignaledChildIsNotSuccessful(): void + { + $compiler = new ScriptedWaitCompiler([]); + + $this->assertTrue($compiler->statusSucceeded(0)); + $this->assertFalse($compiler->statusSucceeded(1 << 8)); + $this->assertFalse($compiler->statusSucceeded(SIGTERM)); + } + + public function testForkFailureStillReapsRunningCompilerProcesses(): void + { + $compiler = new ScriptedWaitCompiler( + [[101, 0, 0]], + [101, -1] + ); + + try { + $compiler->compileInParallelForTest(['first.cc', 'second.cc', 'third.cc'], 2); + $this->fail('The fork failure should fail the parallel compilation'); + } catch (\Exception $e) { + $this->assertStringContainsString('second.cc', $e->getMessage()); + $this->assertStringContainsString('third.cc', $e->getMessage()); + } + + $this->assertSame(1, $compiler->getWaitCallCount()); + } +} + +class ScriptedWaitCompiler extends CompilerTest +{ + private int $waitCallCount = 0; + private int $lastWaitError = 0; + + public function __construct(private array $waitResults, private array $forkResults = []) + { + parent::__construct(ROOT_PATH); + $this->noProgress = true; + } + + protected function pcntlFork(): int + { + return array_shift($this->forkResults); + } + + protected function pcntlWait(?int &$status): int + { + $this->waitCallCount++; + [$pid, $status, $this->lastWaitError] = array_shift($this->waitResults); + return $pid; + } + + protected function pcntlLastError(): int + { + return $this->lastWaitError; + } + + public function waitForTest(): array + { + return $this->waitForCompileChild(); + } + + public function statusSucceeded(int $status): bool + { + return $this->compileChildSucceeded($status); + } + + public function getWaitCallCount(): int + { + return $this->waitCallCount; + } + + public function compileInParallelForTest(array $sourceFiles, int $jobs): array + { + return $this->compileWithPcntl($sourceFiles, $jobs); + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 211d2ebd..dd133582 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1977,6 +1977,15 @@ class CompilerBase implements PropertyAccessContext if ($this->isCurrentConstructor() && !$this->context->inClosure) { $this->fatalError($v, 'Method `' . $this->getCurrentMethodDisplayName() . '()` cannot return a value'); } + if (!$this->context->inClosure && !empty($this->functionDef->returnTypeCheck)) { + $this->checkCompositeTypeAssignment( + $v, + $this->functionDef->returnTypeCheck, + $this->functionDef->returnTypeStr, + $v->expr, + 'return value' + ); + } $expr = $this->parseExprAsValue($v->expr); $returnType = $this->getReturnType(); @@ -5125,6 +5134,16 @@ class CompilerBase implements PropertyAccessContext $type = $this->detectTypeOfExpr($arg->value); $this->assertExprCanBeUsedAsValue($arg->value, 'function argument'); + if (!empty($argInfo->typeCheck)) { + $this->checkCompositeTypeAssignment( + $arg, + $argInfo->typeCheck, + $argInfo->typeStr, + $arg->value, + 'argument `$' . ($argInfo->phpName ?: $this->unescapeVarName($argInfo->name)) . '`' + ); + } + if ($argInfo->byRef) { if ($this->isReferenceWrapperCall($arg->value)) { $inner = $this->unwrapReferenceWrapperCall($arg->value, $arg); @@ -5324,6 +5343,18 @@ class CompilerBase implements PropertyAccessContext } $rightType = $this->detectTypeOfExpr($right); + if (!empty($def->typeCheck) && $this->checkCompositeTypeAssignment( + $left, + $def->typeCheck, + $def->typeStr, + $right, + 'property assignment' + ) && $rightType !== self::TYPE_VAR) { + // A statically known member of the composite type needs no + // Variant runtime guard on this property write. + return $rightExpr; + } + if ($rightType !== self::TYPE_VAR && $this->canAssignStaticTypeToObjectProperty($def, $rightType)) { return $rightExpr; } @@ -7584,6 +7615,135 @@ class CompilerBase implements PropertyAccessContext $this->fatalError($left, "Cannot re-assign $varName from `{$fromType}` to `{$toType}`"); } + /** + * Check a value against a composite PHP type when the value's static type + * is precise enough to prove a mismatch. Composite declarations still use + * Variant in generated C++, so unknown values must be left to the runtime + * type check emitted from the same descriptor. + * + * The outer descriptor list is a union (OR); an allOf entry represents an + * intersection (AND). Nullable is represented by an isNull union member. + */ + protected function checkCompositeTypeAssignment( + NodeAbstract $errorNode, + array $typeCheck, + string $typeStr, + NodeAbstract $value, + string $context + ): bool { + if ($this->compositeTypeMayMatch($value, $typeCheck)) { + return true; + } + + $valueType = $this->staticTypeNameOfExpr($value); + $this->fatalError($errorNode, "Cannot assign {$valueType} to {$context} of type `{$typeStr}`"); + } + + protected function compositeTypeMayMatch(NodeAbstract $value, array $clauses): bool + { + // TYPE_VAR means that the expression is dynamic or its result cannot + // be represented by the current scalar type system. Do not reject it. + if ($this->detectTypeOfExpr($value) === self::TYPE_VAR && !$this->isNullExpr($value)) { + return true; + } + + foreach ($clauses as $clause) { + if ($this->compositeTypeClauseMayMatch($value, $clause)) { + return true; + } + } + return false; + } + + protected function compositeTypeClauseMayMatch(NodeAbstract $value, array $clause): bool + { + if (($clause['kind'] ?? '') === 'allOf') { + foreach ($clause['types'] ?? [] as $entry) { + if (!$this->compositeTypeEntryMayMatch($value, $entry)) { + return false; + } + } + return true; + } + return $this->compositeTypeEntryMayMatch($value, $clause); + } + + protected function compositeTypeEntryMayMatch(NodeAbstract $value, array $entry): bool + { + $kind = $entry['kind'] ?? ''; + if ($kind === 'isNull') { + return $this->isNullExpr($value); + } + + $type = $this->detectTypeOfExpr($value); + return match ($kind) { + 'isInt' => $type === self::TYPE_INT, + 'isFloat' => $type === self::TYPE_FLOAT, + 'isBool' => $type === self::TYPE_BOOL, + 'isString' => $type === self::TYPE_STR, + 'isArray' => $type === self::TYPE_ARRAY, + 'isObject' => $type === self::TYPE_OBJECT, + 'isTrue', 'isFalse' => $type === self::TYPE_BOOL, + 'isResource' => $type === self::TYPE_RESOURCE, + // These checks depend on runtime callable/traversable state unless + // a future value lattice adds those properties. + 'callable', 'iterable' => true, + 'instanceof' => $this->compositeObjectEntryMayMatch($value, $entry), + default => true, + }; + } + + protected function compositeObjectEntryMayMatch(NodeAbstract $value, array $entry): bool + { + if ($this->detectTypeOfExpr($value) !== self::TYPE_OBJECT) { + return false; + } + + $class = $this->detectDeclaredClassOfExpr($value); + if ($class === '') { + return true; + } + + $expected = $entry['class'] ?? ''; + // If the expected class/interface is outside the AOT class graph, + // static analysis cannot prove incompatibility. Keep the runtime + // instanceof check (this is common for extension-provided interfaces). + if ($expected === '' + || (!$this->hasClass($expected) + && !$this->hasInterface($expected) + && !$this->isInternalClass($expected) + && !$this->isInternalInterface($expected))) { + return true; + } + + return $expected === 'static' + ? true + : $this->isObjectClassStaticallyAssignableTo($class, $expected); + } + + protected function isNullExpr(NodeAbstract $expr): bool + { + return $expr instanceof Expr\ConstFetch + && strcasecmp($this->parseIdentifier($expr->name), 'null') === 0; + } + + protected function staticTypeNameOfExpr(NodeAbstract $expr): string + { + if ($this->isNullExpr($expr)) { + return 'null'; + } + $type = $this->detectTypeOfExpr($expr); + return match ($type) { + self::TYPE_INT => 'int', + self::TYPE_FLOAT => 'float', + self::TYPE_BOOL => 'bool', + self::TYPE_STR => 'string', + self::TYPE_ARRAY => 'array', + self::TYPE_OBJECT => 'object', + default => 'mixed', + }; + } + protected function mustNoCall(NodeAbstract $node): void { $nodeFinder = new NodeFinder(); diff --git a/src/Generator/FiberGenerator.php b/src/Generator/FiberGenerator.php index 52e47e85..a0d0c724 100644 --- a/src/Generator/FiberGenerator.php +++ b/src/Generator/FiberGenerator.php @@ -164,6 +164,21 @@ trait FiberGenerator } protected function genFiberGeneratorFunction(Function_|ClassMethod $v, FunctionDef $functionDef, string $nativeName): string + { + $entryContext = $this->context; + $entryIndent = $this->indentLevel; + $entryInGeneratorBody = $this->inGeneratorBody; + + try { + return $this->doGenFiberGeneratorFunction($v, $functionDef, $nativeName); + } finally { + $this->context = $entryContext; + $this->indentLevel = $entryIndent; + $this->inGeneratorBody = $entryInGeneratorBody; + } + } + + private function doGenFiberGeneratorFunction(Function_|ClassMethod $v, FunctionDef $functionDef, string $nativeName): string { $functionDeclCode = self::TYPE_VAR . ' ' . self::PREFIX . $nativeName . '('; if ($this->class) { diff --git a/src/Translator.php b/src/Translator.php index eb76c0f2..456d1fca 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1204,7 +1204,7 @@ CODE; /** * 检查 phpx/src/misc/ 下的源文件是否已有有效缓存,始终生效(除非指定 --force)。 - * 仅检查 .o 文件是否比源文件和 phpx 头文件更新。 + * 缓存必须匹配编译命令和 PHP ABI,且 .o 文件必须不早于源文件和 phpx 头文件。 */ public function hasMiscObjectFileCache(string $cppFile): bool { @@ -1217,6 +1217,16 @@ CODE; return false; } + $metadataFile = $this->getMiscObjectCacheMetadataFile($objectFile); + if (!is_file($metadataFile)) { + return false; + } + + $cachedKey = file_get_contents($metadataFile); + if ($cachedKey === false || trim($cachedKey) !== $this->getMiscObjectCacheKey($cppFile, $objectFile)) { + return false; + } + $objectMtime = filemtime($objectFile); if ($objectMtime <= filemtime($cppFile)) { return false; @@ -1242,6 +1252,41 @@ CODE; return true; } + protected function getMiscObjectCacheMetadataFile(string $objectFile): string + { + return $objectFile . '.typephp-cache'; + } + + protected function getMiscObjectCacheKey(string $sourceFile, string $objectFile): string + { + $abi = [ + 'php_version_id' => PHP_VERSION_ID, + 'php_api_version' => defined('PHP_API_VERSION') ? PHP_API_VERSION : null, + 'zend_module_api' => defined('ZEND_MODULE_API_NO') ? ZEND_MODULE_API_NO : null, + 'php_zts' => defined('PHP_ZTS') ? PHP_ZTS : null, + 'php_debug' => defined('PHP_DEBUG') ? PHP_DEBUG : null, + 'integer_size' => PHP_INT_SIZE, + ]; + + return hash('sha256', $this->buildCompileFileCommand($sourceFile, $objectFile) . "\0" . serialize($abi)); + } + + protected function writeMiscObjectCacheMetadata(string $sourceFile, string $objectFile): void + { + $metadataFile = $this->getMiscObjectCacheMetadataFile($objectFile); + if (file_put_contents($metadataFile, $this->getMiscObjectCacheKey($sourceFile, $objectFile) . PHP_EOL) === false) { + throw new \RuntimeException('Cannot write misc object cache metadata: ' . $metadataFile); + } + } + + protected function invalidateMiscObjectCache(string $objectFile): void + { + $metadataFile = $this->getMiscObjectCacheMetadataFile($objectFile); + if (is_file($metadataFile)) { + unlink($metadataFile); + } + } + public function isPhpxMiscFile(string $cppFile): bool { $miscDir = $this->getPhpxDir() . '/src/misc/'; @@ -1293,35 +1338,12 @@ CODE; return; } - // 检测文件语言类型 - $language = $this->getLanguageFromExtension($cppFile); - - // 使用 Backend 层构建编译命令 - if ($language === null) { - // C++ 文件:使用标准的编译命令构建 - $cmd = $this->getCompilerBackend()->buildCompileCommand( - $cppFile, - $objectFile, - $this->getCompileCommandOptions() - ); - } elseif ($language === 'c') { - // C 文件:使用 buildCCompileCommand,后端自动添加 -x c 或 /TC - $cmd = $this->getCompilerBackend()->buildCCompileCommand( - $cppFile, - $objectFile, - $this->getCCompileCommandOptions() - ); - } else { - // 其他原生源文件(assembler, objective-c, objective-c++) - // 使用 buildNativeCompileCommand,传入语言类型以添加 -x 标志 - $cmd = $this->getCompilerBackend()->buildNativeCompileCommand( - $cppFile, - $objectFile, - $this->getNativeCompileCommandOptions($language), - $language - ); + $isMiscFile = $this->isPhpxMiscFile($cppFile); + if ($isMiscFile) { + $this->invalidateMiscObjectCache($objectFile); } + $cmd = $this->buildCompileFileCommand($cppFile, $objectFile); if (!$parallel) { $this->climate->comment($cmd); } @@ -1339,6 +1361,36 @@ CODE; } $this->error('compile failed: ' . $cppFile); } + + if ($isMiscFile) { + $this->writeMiscObjectCacheMetadata($cppFile, $objectFile); + } + } + + protected function buildCompileFileCommand(string $sourceFile, string $objectFile): string + { + $language = $this->getLanguageFromExtension($sourceFile); + if ($language === null) { + return $this->getCompilerBackend()->buildCompileCommand( + $sourceFile, + $objectFile, + $this->getCompileCommandOptions() + ); + } + if ($language === 'c') { + return $this->getCompilerBackend()->buildCCompileCommand( + $sourceFile, + $objectFile, + $this->getCCompileCommandOptions() + ); + } + + return $this->getCompilerBackend()->buildNativeCompileCommand( + $sourceFile, + $objectFile, + $this->getNativeCompileCommandOptions($language), + $language + ); } public function compile(array $sourceFiles): array @@ -1416,6 +1468,53 @@ CODE; /** * Unix/Linux/macOS 平台并行编译(使用 pcntl) */ + protected function pcntlWait(?int &$status): int + { + return pcntl_wait($status); + } + + protected function pcntlFork(): int + { + return pcntl_fork(); + } + + protected function pcntlLastError(): int + { + return pcntl_get_last_error(); + } + + protected function waitForCompileChild(): array + { + do { + $status = null; + $pid = $this->pcntlWait($status); + $error = $pid === -1 ? $this->pcntlLastError() : 0; + } while ($pid === -1 && defined('PCNTL_EINTR') && $error === PCNTL_EINTR); + + if ($pid === -1) { + $message = function_exists('pcntl_strerror') ? pcntl_strerror($error) : 'error ' . $error; + throw new \RuntimeException('Failed to wait for compiler process: ' . $message); + } + + return [$pid, (int) $status]; + } + + protected function compileChildSucceeded(int $status): bool + { + return pcntl_wifexited($status) && pcntl_wexitstatus($status) === 0; + } + + protected function getCompileChildFailureReason(int $status): string + { + if (pcntl_wifsignaled($status)) { + return 'terminated by signal ' . pcntl_wtermsig($status); + } + if (pcntl_wifexited($status)) { + return 'exited with status ' . pcntl_wexitstatus($status); + } + return 'terminated abnormally'; + } + protected function compileWithPcntl(array $sourceFiles, int $job): array { // 检查 pcntl 扩展是否可用 @@ -1448,9 +1547,16 @@ CODE; $cppFile = array_shift($fileQueue); $objectFile = $this->getObjectFile($cppFile); - $pid = pcntl_fork(); + $pid = $this->pcntlFork(); if ($pid == -1) { - throw new \Exception('Failed to fork process'); + $failedFiles[] = $cppFile; + foreach ($fileQueue as $queuedFile) { + $failedFiles[] = $queuedFile; + } + $compiledCount += count($fileQueue) + 1; + $fileQueue = []; + $this->climate->red('Failed to fork compiler process; no additional files will be scheduled'); + break; } if ($pid === 0) { // 子进程:执行编译 @@ -1473,54 +1579,54 @@ CODE; // 等待任意一个子进程完成 if ($runningProcesses > 0) { - $status = null; - $pid = pcntl_wait($status); - if ($pid > 0) { - $processInfo = $processPipes[$pid] ?? null; - unset($processPipes[$pid]); - $runningProcesses--; - - $exitCode = pcntl_wexitstatus($status); - if ($exitCode !== 0) { - $failedFile = $processInfo['file'] ?? 'unknown'; - $failedFiles[] = $failedFile; - echo PHP_EOL; - $this->climate->red("Compilation failed: {$failedFile}"); - echo PHP_EOL; - } else { - if ($processInfo) { - $objectFiles[] = $processInfo['object']; - } - } - $compiledCount++; - if ($this->noProgress) { - $percent = intval($compiledCount / $totalFiles * 100); - $file = $processInfo['file'] ?? 'unknown'; - $fileShorted = $this->removeCommonPrefix($this->buildDir, $file); - $this->climate->white("[{$compiledCount}/{$totalFiles}] {$percent}% {$fileShorted}"); - } else { - $progress->renderInPlace($compiledCount, $totalFiles, 'Compiling'); - } - } - } - } - - // 确保所有子进程都已结束 - while ($runningProcesses > 0) { - $status = null; - $pid = pcntl_wait($status); - if ($pid > 0) { + [$pid, $status] = $this->waitForCompileChild(); $processInfo = $processPipes[$pid] ?? null; unset($processPipes[$pid]); $runningProcesses--; + + if (!$this->compileChildSucceeded($status)) { + $failedFile = $processInfo['file'] ?? 'unknown'; + $failedFiles[] = $failedFile; + echo PHP_EOL; + $reason = $this->getCompileChildFailureReason($status); + $this->climate->red("Compilation failed: {$failedFile} ({$reason})"); + echo PHP_EOL; + } elseif ($processInfo) { + $objectFiles[] = $processInfo['object']; + } $compiledCount++; - if ($this->noProgress && $processInfo) { + if ($this->noProgress) { $percent = intval($compiledCount / $totalFiles * 100); - $this->climate->darkGray("[{$compiledCount}/{$totalFiles}] {$percent}% {$processInfo['file']}"); + $file = $processInfo['file'] ?? 'unknown'; + $fileShorted = $this->removeCommonPrefix($this->buildDir, $file); + $this->climate->white("[{$compiledCount}/{$totalFiles}] {$percent}% {$fileShorted}"); + } else { + $progress->renderInPlace($compiledCount, $totalFiles, 'Compiling'); } } } + // 确保所有子进程都已结束 + while ($runningProcesses > 0) { + [$pid, $status] = $this->waitForCompileChild(); + $processInfo = $processPipes[$pid] ?? null; + unset($processPipes[$pid]); + $runningProcesses--; + $compiledCount++; + if (!$this->compileChildSucceeded($status)) { + $failedFile = $processInfo['file'] ?? 'unknown'; + $failedFiles[] = $failedFile; + $reason = $this->getCompileChildFailureReason($status); + $this->climate->red("Compilation failed: {$failedFile} ({$reason})"); + } elseif ($processInfo) { + $objectFiles[] = $processInfo['object']; + } + if ($this->noProgress && $processInfo) { + $percent = intval($compiledCount / $totalFiles * 100); + $this->climate->darkGray("[{$compiledCount}/{$totalFiles}] {$percent}% {$processInfo['file']}"); + } + } + echo PHP_EOL; if (!empty($failedFiles)) { @@ -3422,9 +3528,11 @@ CODE; } if ($this->functionDef->generator) { - $code = $this->genFiberGeneratorFunction($v, $this->functionDef, $name); - $this->resetFunction(); - return $code; + try { + return $this->genFiberGeneratorFunction($v, $this->functionDef, $name); + } finally { + $this->resetFunction(); + } } // Build SSA/e-SSA analysis for this function diff --git a/tests/aot/type_decl/intersection-param-check.phpt b/tests/aot/type_decl/intersection-param-check.phpt index ad052f9f..7281fa3d 100644 --- a/tests/aot/type_decl/intersection-param-check.phpt +++ b/tests/aot/type_decl/intersection-param-check.phpt @@ -13,13 +13,17 @@ function expect_both(IA&IB $value): void { var_dump(get_class($value)); } +function dynamic_value(mixed $value): mixed { + return $value; +} + function main() { expect_both(new Both()); $errors = []; try { - expect_both(new OnlyA()); + expect_both(dynamic_value(new OnlyA())); } catch (\TypeError $e) { $errors[] = $e->getMessage(); } diff --git a/tests/aot/type_decl/union-param-check.phpt b/tests/aot/type_decl/union-param-check.phpt index f0668a0b..1808a8dd 100644 --- a/tests/aot/type_decl/union-param-check.phpt +++ b/tests/aot/type_decl/union-param-check.phpt @@ -27,6 +27,10 @@ function expect_bool_or_array(bool|array $x): void { var_dump($x); } +function dynamic_value(mixed $value): mixed { + return $value; +} + function main() { // Valid calls - should pass expect_int_or_string(42); @@ -47,25 +51,25 @@ function main() { $errors = []; try { - expect_int_or_string(3.14); + expect_int_or_string(dynamic_value(3.14)); } catch (\TypeError $e) { $errors[] = $e->getMessage(); } try { - expect_int_or_string([]); + expect_int_or_string(dynamic_value([])); } catch (\TypeError $e) { $errors[] = $e->getMessage(); } try { - expect_nullable_int("hello"); + expect_nullable_int(dynamic_value("hello")); } catch (\TypeError $e) { $errors[] = $e->getMessage(); } try { - expect_bool_or_array(42); + expect_bool_or_array(dynamic_value(42)); } catch (\TypeError $e) { $errors[] = $e->getMessage(); } diff --git a/tests/aot/type_hits/009.phpt b/tests/aot/type_hits/009.phpt index 28763bbe..ffec0626 100644 --- a/tests/aot/type_hits/009.phpt +++ b/tests/aot/type_hits/009.phpt @@ -2,6 +2,8 @@ type hits: instance property type check message includes class name --FILE-- union = null; + $this->union = dynamic_value(null); } catch (TypeError $e) { var_dump($e->getMessage()); } diff --git a/tests/aot/type_hits/010.phpt b/tests/aot/type_hits/010.phpt index ab024bd4..d6133ccd 100644 --- a/tests/aot/type_hits/010.phpt +++ b/tests/aot/type_hits/010.phpt @@ -4,6 +4,8 @@ type hits: property coalesce assignment uses runtime type check USE_ZEND_ALLOC=0 --FILE-- union ??= null; + $this->union ??= dynamic_value(null); } catch (TypeError $e) { var_dump($e->getMessage()); } $this->union = "ok"; - $this->union ??= null; + $this->union ??= dynamic_value(null); var_dump($this->union); } }