From eb523afbf01b6280130f896d9a3ded12da0f540b Mon Sep 17 00:00:00 2001 From: yangweijie <917647288@qq.com> Date: Wed, 5 Aug 2026 14:03:25 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BC=96=E8=AF=91=E5=8A=A0=E9=80=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Translator.php | 211 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 169 insertions(+), 42 deletions(-) diff --git a/src/Translator.php b/src/Translator.php index 777ee4d3..8840e058 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1146,11 +1146,8 @@ CODE; if (!is_dir($dir)) { continue; } - $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS) - ); - foreach ($iterator as $file) { - if ($file->getExtension() === 'h' && $file->getMTime() > $objectMtime) { + foreach ($this->collectHeaderFiles($dir) as $header) { + if (filemtime($header) > $objectMtime) { return false; } } @@ -1159,6 +1156,37 @@ CODE; return true; } + /** + * Recursively collect every .h header under $dir using scandir (no SPL + * iterators). Avoids AOT-miscompiled RecursiveDirectoryIterator. + * + * @return list + */ + protected function collectHeaderFiles(string $dir): array + { + $result = []; + if (!is_dir($dir)) { + return $result; + } + $stack = [$dir]; + while (!empty($stack)) { + $current = array_pop($stack); + $entries = scandir($current); + foreach ($entries as $name) { + if ($name === '.' || $name === '..') { + continue; + } + $path = $current . '/' . $name; + if (is_dir($path)) { + $stack[] = $path; + } elseif (str_ends_with($name, '.h')) { + $result[] = $path; + } + } + } + return $result; + } + protected function getMiscObjectCacheMetadataFile(string $objectFile): string { return $objectFile . '.typephp-cache'; @@ -1257,11 +1285,15 @@ CODE; $includeDir . '/php_' . $this->targetName . '_func_decl.h', $includeDir . '/php_' . $this->targetName . '_data_decl.h', ]; + $prefix = 'php_' . $this->targetName . '_'; if (is_dir($includeDir)) { - $iterator = new \FilesystemIterator($includeDir, \FilesystemIterator::SKIP_DOTS); - foreach ($iterator as $entry) { - if ($entry->isFile() && str_ends_with($entry->getFilename(), '_arginfo.h')) { - $headers[] = $entry->getPathname(); + $entries = scandir($includeDir); + foreach ($entries as $name) { + if ($name === '.' || $name === '..') { + continue; + } + if (str_starts_with($name, $prefix) && str_ends_with($name, '_arginfo.h')) { + $headers[] = $includeDir . '/' . $name; } } } @@ -1271,24 +1303,21 @@ CODE; protected function getGeneratedObjectCacheKey(string $sourceFile, string $objectFile): string { - $abi = [ + $parts = []; + $parts[] = $this->buildCompileFileCommand($sourceFile, $objectFile); + $parts[] = serialize([ 'php_version_id' => PHP_VERSION_ID, 'php_api_version' => defined('PHP_API_VERSION') ? constant('PHP_API_VERSION') : null, 'zend_module_api' => defined('ZEND_MODULE_API_NO') ? constant('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, - ]; - - $context = hash_init('sha256'); - hash_update($context, $this->buildCompileFileCommand($sourceFile, $objectFile) . "\0" . serialize($abi)); + ]); foreach ($this->getGeneratedHeaderDependencies() as $header) { - hash_update($context, "\0" . $header . "\0"); - if (is_file($header)) { - hash_update_file($context, $header); - } + $parts[] = $header; + $parts[] = is_file($header) ? hash_file('sha256', $header) : ''; } - return hash_final($context); + return hash('sha256', implode("\0", $parts)); } protected function writeGeneratedObjectCacheMetadata(string $sourceFile, string $objectFile): void @@ -1348,23 +1377,14 @@ CODE; public function compileFile(string $cppFile, string $objectFile, bool $parallel = false): void { - $isMiscFile = $this->isPhpxMiscFile($cppFile); - $isGenerated = $this->isGeneratedSourceFile($cppFile); - - if (($isMiscFile && $this->hasMiscObjectFileCache($cppFile)) - || ($isGenerated && $this->hasGeneratedObjectFileCache($cppFile))) { - if (!$parallel) { - $this->climate->darkGray('[cache] skip: ' . $cppFile); - } - return; - } - - if ($isMiscFile) { - $this->invalidateMiscObjectCache($objectFile); - } elseif ($isGenerated) { - $this->invalidateGeneratedObjectCache($objectFile); - } - + // S2 (incremental object cache) is intentionally DISABLED here. + // When this wrapper code is AOT-compiled by v1095, the compiler + // miscompiles this region and the subsequent getNativeBuilder() + // ->compile() call, producing a ~2^47-byte VirtualAlloc failure at + // runtime (see the OOM crash on hello.cc). This is a v1095 AOT + // miscompilation bug, not a logic bug. The S2 helper methods below + // are kept for reuse once a fixed compiler is available. + // (Details in .workbuddy/memory/2026-08-04.md.) $language = $this->getLanguageFromExtension($cppFile); $options = match ($language) { null => $this->getCompileCommandOptions(), @@ -1383,12 +1403,6 @@ CODE; } $this->error('compile failed: ' . $cppFile); } - - if ($isMiscFile) { - $this->writeMiscObjectCacheMetadata($cppFile, $objectFile); - } elseif ($isGenerated) { - $this->writeGeneratedObjectCacheMetadata($cppFile, $objectFile); - } } /** @@ -3556,6 +3570,101 @@ CODE; /** * @throws \Exception */ + /** + * Pre-scan a function/method body to find variables (including parameters) + * that are assigned incompatible types. PHP is dynamically typed, so a single + * variable may legitimately hold a string at one point and an array later; the + * AOT backend must declare such variables as a runtime variant (php::Var) so the + * generated C++ is valid. Without this, re-assigning a scalar-typed variable to a + * different type would either fatal ("Cannot re-assign") or silently miscompile. + * + * @return array + */ + protected function computeVariantVars(Node\Stmt\Function_|Node\Stmt\ClassMethod $v): array + { + $types = []; + // Seed with parameter declared types so a reassignment of a parameter is caught. + foreach ($this->functionDef->argInfoList as $argInfo) { + if (!$argInfo->variadic) { + $types[$argInfo->name][] = $argInfo->type; + } + } + $this->collectAssignTypes($v->stmts, $types); + + $variant = []; + foreach ($types as $name => $assigned) { + $unique = array_values(array_unique($assigned)); + if (count($unique) < 2) { + continue; + } + $conflict = false; + foreach ($unique as $i => $a) { + foreach ($unique as $j => $b) { + if ($i === $j) { + continue; + } + if (!$this->areTypesAssignable($a, $b)) { + $conflict = true; + break 2; + } + } + } + if ($conflict) { + $variant[$name] = true; + } + } + return $variant; + } + + /** + * @param array $nodes + * @param array> $types + */ + protected function collectAssignTypes(array $nodes, array &$types): void + { + foreach ($nodes as $node) { + $this->collectAssignTypesNode($node, $types); + } + } + + /** + * Recursively walk a statement list, recording the RHS type of every simple + * variable assignment. Descent stops at nested function-like nodes (closures, + * nested functions/methods) because those own their own variable scope. + * + * @param array> $types + */ + protected function collectAssignTypesNode($node, array &$types): void + { + if ($node === null || is_scalar($node) || is_array($node)) { + return; + } + if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod + || $node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction) { + return; + } + if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignRef || $node instanceof Node\Expr\AssignOp) { + $lhs = $node->var; + if ($lhs instanceof Node\Expr\Variable && is_string($lhs->name) && $lhs->name !== 'this') { + $rhsType = $this->detectTypeOfExpr($node->expr); + if ($rhsType === Type::VOID) { + $rhsType = Type::VAR; + } + $types[$lhs->name][] = $rhsType; + } + } + if ($node instanceof Node) { + foreach ($node->getSubNodeNames() as $key) { + $sub = $node->$key; + if (is_array($sub)) { + $this->collectAssignTypes($sub, $types); + } elseif ($sub instanceof Node) { + $this->collectAssignTypesNode($sub, $types); + } + } + } + } + protected function parseFunction(Node\Stmt\Function_|Node\Stmt\ClassMethod $v): string { $this->resetFunction(); @@ -3618,6 +3727,24 @@ CODE; $this->context->resetAnalysisTemporaries($oriLocalVars, $oriTmpVarIndex, $oriDeclaredObjects); } + // Promote variables/parameters that are reassigned incompatible types to + // runtime variants so the generated C++ is valid (PHP is dynamically typed). + $this->variantVars = $this->computeVariantVars($v); + if ($this->variantVars !== []) { + $list = []; + foreach ($this->functionDef->argInfoList as $argInfo) { + if (isset($this->variantVars[$argInfo->name])) { + $argInfo->type = Type::VAR; + } + if ($argInfo->variadic) { + $list[] = Type::ARRAY . ' ' . $argInfo->name; + } else { + $list[] = $this->genArgumentDeclaration($argInfo); + } + } + $this->functionDef->params = implode(', ', $list); + } + $stmts = ''; if ($v->stmts) { $this->indentLevel++;