|
|
|
|
@ -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<string> |
|
|
|
|
*/ |
|
|
|
|
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<string, true> |
|
|
|
|
*/ |
|
|
|
|
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<int, mixed> $nodes |
|
|
|
|
* @param array<string, list<string>> $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<string, list<string>> $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++; |
|
|
|
|
|