diff --git a/src/Backend/Clang.php b/src/Backend/Clang.php index ea4177e4..5abecd70 100644 --- a/src/Backend/Clang.php +++ b/src/Backend/Clang.php @@ -85,6 +85,16 @@ class Clang extends GccLikeBackend return '-fsanitize=' . $sanitizer; } + public function getPrecompiledHeaderArtifact(string $headerFile): string + { + return dirname($headerFile) . DIRECTORY_SEPARATOR . pathinfo($headerFile, PATHINFO_FILENAME) . '.pch'; + } + + protected function formatPrecompiledHeaderFlag(array $precompiledHeader): string + { + return ' -include-pch ' . escapeshellarg($precompiledHeader['artifact']); + } + protected function getPICFlag(array $config): string { if ($this->platform instanceof Windows) { diff --git a/src/Backend/CompilerBackend.php b/src/Backend/CompilerBackend.php index 79963d69..0e3f8a64 100644 --- a/src/Backend/CompilerBackend.php +++ b/src/Backend/CompilerBackend.php @@ -40,6 +40,16 @@ abstract class CompilerBackend */ abstract public function getLinkerCommand(): string; + public function supportsPrecompiledHeaders(): bool + { + return false; + } + + public function getPrecompiledHeaderArtifact(string $headerFile): string + { + throw new \LogicException($this->getName() . ' does not support precompiled headers'); + } + /** * 编译单个文件 */ diff --git a/src/Backend/GccLikeBackend.php b/src/Backend/GccLikeBackend.php index 6ebe5a8f..1fce1c68 100644 --- a/src/Backend/GccLikeBackend.php +++ b/src/Backend/GccLikeBackend.php @@ -26,6 +26,16 @@ abstract class GccLikeBackend extends CompilerBackend return $this->compilerCommand; } + public function supportsPrecompiledHeaders(): bool + { + return true; + } + + public function getPrecompiledHeaderArtifact(string $headerFile): string + { + return $headerFile . '.gch'; + } + // ──── 钩子方法(子类覆盖点) ──── /** 编译器特定的前缀标志(如 MSVC 兼容模式) */ @@ -113,9 +123,19 @@ abstract class GccLikeBackend extends CompilerBackend $cmd .= ' -flto'; } + if ($includeCppStd && !empty($config['precompiled_header'])) { + $cmd .= $this->formatPrecompiledHeaderFlag($config['precompiled_header']); + } + return $cmd; } + /** @param array{header: string, artifact: string} $precompiledHeader */ + protected function formatPrecompiledHeaderFlag(array $precompiledHeader): string + { + return ' -include ' . escapeshellarg($precompiledHeader['header']); + } + /** 获取平台特定的链接选项 */ protected function getPlatformLinkFlags(array $config): string { diff --git a/src/Build/NativeBuilder.php b/src/Build/NativeBuilder.php index 4e804930..30108615 100644 --- a/src/Build/NativeBuilder.php +++ b/src/Build/NativeBuilder.php @@ -76,15 +76,26 @@ final readonly class NativeBuilder Closure $succeeded, ?Closure $completed = null, ): array { - $queue = array_values($sources); + // Keep most workers on the largest translation units to reduce the + // parallel tail, but reserve one fast lane for small files so progress + // remains visible while the expensive units are still compiling. + $queue = SourceCompileQueue::largestFirst($sources); $running = []; $objects = []; $failures = []; $completedCount = 0; + $largeTaskCount = 0; + $largeLaneLimit = max(1, $jobs - 1); while ($queue !== [] || $running !== []) { while (count($running) < $jobs && $queue !== []) { - $source = array_shift($queue); + if ($largeTaskCount < $largeLaneLimit) { + $source = array_shift($queue); + $lane = 'large'; + } else { + $source = array_pop($queue); + $lane = 'small'; + } $object = $objectFile($source); $pid = $fork(); if ($pid === -1) { @@ -101,7 +112,10 @@ final readonly class NativeBuilder exit(1); } } - $running[$pid] = ['source' => $source, 'object' => $object]; + $running[$pid] = ['source' => $source, 'object' => $object, 'lane' => $lane]; + if ($lane === 'large') { + $largeTaskCount++; + } } if ($running === []) { break; @@ -112,6 +126,9 @@ final readonly class NativeBuilder if ($task === null) { continue; } + if ($task['lane'] === 'large') { + $largeTaskCount--; + } $success = $succeeded($status); if ($success) { $objects[] = $task['object']; diff --git a/src/Build/NativeCommandOptionsTrait.php b/src/Build/NativeCommandOptionsTrait.php index a65baf81..358609fe 100644 --- a/src/Build/NativeCommandOptionsTrait.php +++ b/src/Build/NativeCommandOptionsTrait.php @@ -12,6 +12,9 @@ use TypePhp\Metadata\Constants; trait NativeCommandOptionsTrait { + /** @var null|array{header: string, artifact: string} */ + protected ?array $precompiledHeader = null; + protected function getCommonCompileCommandOptions(): CompileOptions { $includePaths = $this->getIncludePaths(); @@ -43,10 +46,16 @@ trait NativeCommandOptionsTrait protected function getCompileCommandOptions(): CompileOptions { $options = $this->getCommonCompileCommandOptions(); - return $options + $options = $options ->with('cpp_std', $this->cxxStd) ->with('cxxflags', $this->cxxFlags) ->with('suppressed_warnings', Constants::MSVC_SUPPRESSED_WARNINGS ?? []); + + if ($this->precompiledHeader !== null) { + $options = $options->with('precompiled_header', $this->precompiledHeader); + } + + return $options; } protected function getCCompileCommandOptions(): CompileOptions diff --git a/src/Build/PrecompiledHeaderManager.php b/src/Build/PrecompiledHeaderManager.php new file mode 100644 index 00000000..53d00f04 --- /dev/null +++ b/src/Build/PrecompiledHeaderManager.php @@ -0,0 +1,104 @@ + $headers + * @param list $dependencyDirectories + * @return array{header: string, artifact: string, cached: bool, command: string} + */ + public function prepare( + array $headers, + array $dependencyDirectories, + string $cacheDirectory, + CompileOptions $options, + ): array { + if (!$this->backend->supportsPrecompiledHeaders()) { + throw new \LogicException($this->backend->getName() . ' does not support precompiled headers'); + } + + $fingerprint = $this->buildFingerprint($headers, $dependencyDirectories, $options); + $directory = rtrim($cacheDirectory, '/\\') . DIRECTORY_SEPARATOR . $fingerprint; + if (!is_dir($directory) && !mkdir($directory, 0777, true) && !is_dir($directory)) { + throw new \RuntimeException('Cannot create precompiled header cache directory: ' . $directory); + } + + $headerFile = $directory . DIRECTORY_SEPARATOR . 'typephp_pch.hpp'; + $artifact = $this->backend->getPrecompiledHeaderArtifact($headerFile); + $source = "#pragma once\n"; + foreach ($headers as $header) { + $source .= '#include <' . $header . ">\n"; + } + if (!is_file($headerFile) || file_get_contents($headerFile) !== $source) { + if (file_put_contents($headerFile, $source) === false) { + throw new \RuntimeException('Cannot write precompiled header: ' . $headerFile); + } + } + + if (is_file($artifact)) { + return ['header' => $headerFile, 'artifact' => $artifact, 'cached' => true, 'command' => '']; + } + + $result = $this->builder->compile($headerFile, $artifact, $options, 'c++-header', true); + if ($result['status'] !== 0 || !is_file($artifact)) { + $message = implode(PHP_EOL, $result['output']); + throw new \RuntimeException('Failed to build PHPX precompiled header' . ($message === '' ? '' : ': ' . $message)); + } + + return ['header' => $headerFile, 'artifact' => $artifact, 'cached' => false, 'command' => $result['command']]; + } + + /** @param list $headers @param list $dependencyDirectories */ + private function buildFingerprint(array $headers, array $dependencyDirectories, CompileOptions $options): string + { + $compilerVersion = []; + exec(escapeshellcmd($this->backend->getCompilerCommand()) . ' --version 2>&1', $compilerVersion); + $context = hash_init('sha256'); + hash_update($context, $this->backend::class . "\0" . implode("\n", $compilerVersion) . "\0"); + $optionValues = $options->toArray(); + // prof_output only affects code when profiling is enabled. Keeping a + // target-specific inactive filename here would defeat PCH reuse across + // projects with otherwise identical native build configurations. + if (empty($optionValues['enable_profiler'])) { + unset($optionValues['prof_output']); + } + unset($optionValues['precompiled_header']); + hash_update($context, serialize($optionValues) . "\0" . implode("\0", $headers)); + + $files = []; + foreach ($dependencyDirectories as $directory) { + if (!is_dir($directory)) { + continue; + } + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS) + ); + foreach ($iterator as $file) { + if (!$file->isFile() || preg_match('/\.(?:h|hh|hpp|hxx|inc)$/i', $file->getFilename()) !== 1) { + continue; + } + $files[] = $file->getPathname(); + } + } + sort($files, SORT_STRING); + foreach ($files as $file) { + hash_update($context, $file . "\0"); + if (!hash_update_file($context, $file)) { + throw new \RuntimeException('Cannot fingerprint precompiled-header dependency: ' . $file); + } + hash_update($context, "\0"); + } + + return substr(hash_final($context), 0, 24); + } +} diff --git a/src/Build/SourceCompileQueue.php b/src/Build/SourceCompileQueue.php new file mode 100644 index 00000000..86af296b --- /dev/null +++ b/src/Build/SourceCompileQueue.php @@ -0,0 +1,22 @@ + $sources @return list */ + public static function largestFirst(array $sources): array + { + $entries = []; + foreach (array_values($sources) as $index => $source) { + $size = @filesize($source); + $entries[] = ['source' => $source, 'size' => $size === false ? -1 : $size, 'index' => $index]; + } + + usort($entries, static function (array $left, array $right): int { + return ($right['size'] <=> $left['size']) ?: ($left['index'] <=> $right['index']); + }); + + return array_column($entries, 'source'); + } +} diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index d006e6bb..67d57afe 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -397,15 +397,16 @@ trait AssignOpTrait if ($node->var instanceof Expr\PropertyFetch && ($setter = $this->getPropertyHookSetter($node->var)) !== null && ($getter = $this->getPropertyHookGetter($node->var)) !== null) { - $right = $this->parseExprAsValue($node->expr); $read = $this->emitPropertyHookGetterCall($node->var, $getter); $tmp = $this->genTmpVarName(); $this->addLocalVar($tmp, Type::VAR); $binaryOp = $this->removeAssignOp($op); $value = match ($binaryOp) { - '.' => 'php::concat(' . $read . ', ' . $right . ')', - '**' => 'php::fn::pow(' . $read . ', ' . $right . ')', - default => $read . ' ' . $binaryOp . ' (' . $right . ')', + '.' => $this->parseFlattenedConcat($node->expr, [ + $this->prepareConcatOperand($read, $this->detectTypeOfExpr($node->var)), + ]), + '**' => 'php::fn::pow(' . $read . ', ' . $this->parseExprAsValue($node->expr) . ')', + default => $read . ' ' . $binaryOp . ' (' . $this->parseExprAsValue($node->expr) . ')', }; $call = $this->emitPropertyHookSetterCall($node->var, $setter, new Expr\Variable($tmp)); return '((' . $tmp . ' = ' . $value . ', ' . $call . '), ' . $tmp . ')'; @@ -417,7 +418,7 @@ trait AssignOpTrait } $var = $this->parseWritableIdentifier($node->var); - $expr = $this->parseIdentifier($node->expr); + $expr = $this->isAssignOpConcat($op) ? '' : (string) $this->parseIdentifier($node->expr); if ($this->isVarExpr($node->var)) { if (!$this->hasVar($var)) { @@ -439,7 +440,9 @@ trait AssignOpTrait if ($this->isArrayVar($node->var)) { $this->fatalError($node->var, 'Cannot concat string to array'); } - return $var . ' = php::concat(' . $var . ', ' . $rightExprStr . ')'; + return $var . ' = ' . $this->parseFlattenedConcat($node->expr, [ + $this->prepareConcatOperand($var, $type), + ]); } if ($this->isAssignOpPow($op)) { $powExpr = 'php::fn::pow(' . $var . ', ' . $rightExprStr . ')'; @@ -467,9 +470,10 @@ trait AssignOpTrait $binaryOp = $this->removeAssignOp($op); if ($binaryOp === '.') { - $this->context->beforeStmtLines[] = "{$tmpVar} = php::concat(" . - $this->convertVarType($tmpVar, $readVar) . ', ' . - $this->convertExprType($expr, $type, $rightType) . ');'; + $this->context->beforeStmtLines[] = "{$tmpVar} = " . + $this->parseFlattenedConcat($node->expr, [ + $this->prepareConcatOperand($this->convertVarType($tmpVar, $readVar), $type), + ]) . ';'; } elseif ($type === Type::BIGINT || $type === Type::DECIMAL || $type === Type::BIGFLOAT) { $bigAssign = $this->parseBigAssignOpExpr($readVar, $type, $expr, $rightType, $binaryOp, $node->var, $node->expr); $this->context->beforeStmtLines[] = "{$tmpVar} = {$bigAssign};"; @@ -495,7 +499,10 @@ trait AssignOpTrait $this->addLocalVar($tmpVar, Type::VAR); $readProperty = $this->emitDynamicPropertyFetchRead($node->var, $propertyWriteTarget); if ($this->isAssignOpConcat($op)) { - $this->context->beforeStmtLines[] = "{$tmpVar} = php::concat({$readProperty}, {$expr});"; + $this->context->beforeStmtLines[] = "{$tmpVar} = " . + $this->parseFlattenedConcat($node->expr, [ + $this->prepareConcatOperand($readProperty, $this->detectTypeOfExpr($node->var)), + ]) . ';'; } elseif ($this->isAssignOpPow($op)) { $this->context->beforeStmtLines[] = "{$tmpVar} = php::fn::pow({$readProperty}, {$expr});"; } else { @@ -506,7 +513,12 @@ trait AssignOpTrait } if ($this->isAssignOpConcat($op)) { - return $var . '.append(' . $expr . ')'; + $items = []; + $this->flattenConcatExpr($node->expr, $items); + if (count($items) === 1) { + return $var . '.append(' . $this->parseExprAsValue($node->expr) . ')'; + } + return $var . ' = php::toString(' . $this->parseFlattenedConcat($node->expr, [$var]) . ')'; } return $var . ' ' . $op . ' (' . $expr . ')'; } diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index 03f0f6a9..e57f987c 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -254,20 +254,38 @@ trait BinaryOpTrait } protected function parseBinaryOpConcat(Expr\BinaryOp\Concat $expr): string + { + return $this->parseFlattenedConcat($expr); + } + + protected function parseFlattenedConcat(NodeAbstract $expr, array $prefixExpressions = []): string { $items = []; $this->flattenConcatExpr($expr, $items); - $argList = []; + $argList = $prefixExpressions; foreach ($items as $item) { $type = $this->detectTypeOfExpr($item); - $argList[] = $this->convertExprToStringByType($this->parseExprAsValue($item), $type); + $parsed = $this->parseExprAsValue($item); + $argList[] = $this->prepareConcatOperand($parsed, $type); + } + + return Symbol::concat() . '({' . implode(', ', $argList) . '})'; + } + + protected function prepareConcatOperand(string $expr, string $type): string + { + if (in_array($type, [Type::STR, Type::INT, Type::FLOAT, Type::BOOL], true)) { + return $expr; } - return Symbol::concat() . '(' . Symbol::argList() . '{' . implode(', ', $argList) . '})'; + // Keep conversions of objects/arrays/any values at their original + // operand position. Moving them into concat() would evaluate all later + // operands before __toString() or a conversion error is triggered. + return $this->convertExprToStringByType($expr, $type); } - private function flattenConcatExpr(NodeAbstract $expr, array &$items): void + protected function flattenConcatExpr(NodeAbstract $expr, array &$items): void { if ($expr instanceof Expr\BinaryOp\Concat) { $this->flattenConcatExpr($expr->left, $items); diff --git a/src/Parser/ForeachTrait.php b/src/Parser/ForeachTrait.php index 5aee8ce7..df1c990f 100644 --- a/src/Parser/ForeachTrait.php +++ b/src/Parser/ForeachTrait.php @@ -126,17 +126,44 @@ trait ForeachTrait return $code; } + protected function parseForeachIterable(Foreach_ $node, string $iterableVar): string + { + $iterator = $this->genTmpVarName(); + $byRef = $node->byRef ? 'true' : 'false'; + $code = "php::ForeachIterator $iterator{{$iterableVar}, $byRef};" . PHP_EOL; + $code .= "while ($iterator.next()) {" . PHP_EOL; + $this->indentLevel++; + + $code .= $this->parseForeachKeyAssignment($node, $iterator . '.key()'); + $code .= $this->parseForeachValueAssignment( + $node, + $iterator . '.value()', + $iterator . '.valueRef()', + ); + + $body = $this->parseForeachBody($node); + $this->indentLevel--; + + $code .= $this->parseBeforeStmtLines() . PHP_EOL; + $code .= $body . PHP_EOL; + $code .= $this->getIndent() . '}'; + + return $code; + } + protected function parseForeach(Foreach_ $node): string { if ($this->isVarExpr($node->expr)) { $name = $this->parseIdentifier($node->expr); if ($this->hasVar($name)) { $type = $this->getVarType($name); - if ($type === Type::OBJECT) { + if ($type === Type::ARRAY) { + return $this->parseForeachArray($node, $name); + } elseif ($type === Type::OBJECT) { if ($node->byRef) { $this->fatalError($node, 'Cannot use & with foreach'); } - return $this->parseForeachObject($node); + return $this->parseForeachIterable($node, $name); } elseif ($this->isStdContainerType($type)) { return $this->parseForeachStdContainer($node); } @@ -148,32 +175,10 @@ trait ForeachTrait $code .= $this->parseBeforeStmtLines() . PHP_EOL; $iterableVar = $this->genTmpVarName(); - $arrayVar = $this->genTmpVarName(); - $objectVar = $this->genTmpVarName(); $this->addLocalVar($iterableVar, Type::VAR); - $this->addLocalVar($arrayVar, Type::ARRAY); - $this->addLocalVar($objectVar, Type::OBJECT); $code .= $iterableVar . ' = ' . $expr . ';' . PHP_EOL; - $code .= 'if (' . $iterableVar . '.isArray()) {' . PHP_EOL; - $this->indentLevel++; - $code .= $this->getIndent() . $arrayVar . ' = ' . $iterableVar . ';' . PHP_EOL; - $code .= $this->parseForeachArray($node, $arrayVar) . PHP_EOL; - $this->indentLevel--; - $code .= $this->getIndent() . '} else if (' . $iterableVar . '.isObject()) {' . PHP_EOL; - $this->indentLevel++; - $code .= $this->getIndent() . $objectVar . ' = ' . $iterableVar . ';' . PHP_EOL; - if ($node->byRef) { - $code .= $this->getIndent() . 'php::throwException(zend_ce_error, "Cannot use & with foreach");' . PHP_EOL; - } else { - $code .= $this->parseForeachObject($node, $objectVar); - } - $this->indentLevel--; - $code .= $this->getIndent() . '} else {' . PHP_EOL; - $this->indentLevel++; - $code .= $this->getIndent() . 'php::throwException(zend_ce_type_error, "foreach() argument must be of type array|object");' . PHP_EOL; - $this->indentLevel--; - $code .= $this->getIndent() . '}'; + $code .= $this->parseForeachIterable($node, $iterableVar); return $code; } @@ -184,4 +189,3 @@ trait ForeachTrait * 某些情况下高性能计算,可能需要使用原生类型,使用 $a = std::int(0) 来显式地使用原生类型 */ } - diff --git a/src/Parser/TypeConversionTrait.php b/src/Parser/TypeConversionTrait.php index 6bed4424..6494a4bf 100644 --- a/src/Parser/TypeConversionTrait.php +++ b/src/Parser/TypeConversionTrait.php @@ -17,6 +17,9 @@ trait TypeConversionTrait { protected function convertExprToStringByType(string $expr, $type): string { + if ($type === Type::STR) { + return $expr; + } if ($type === Type::BIGINT) { return 'php::BigInt::toString(' . $expr . ')'; } @@ -108,6 +111,9 @@ trait TypeConversionTrait protected function convertStringExpr(string $expr): string { + if (preg_match('/^_literal_strings\[\d+\]$/', $expr) === 1) { + return $expr; + } if (!$this->isClosedExpr($expr, 'php::toString')) { return 'php::toString(' . $expr . ')'; } diff --git a/src/Translator.php b/src/Translator.php index 782bd5af..4362ba8a 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -16,6 +16,7 @@ use TypePhp\Backend\CompilerFactory; use TypePhp\Build\FileScanner; use TypePhp\Build\NativeCommandOptionsTrait; use TypePhp\Build\NativeBuilder; +use TypePhp\Build\PrecompiledHeaderManager; use TypePhp\Build\SourcePipelineTrait; use TypePhp\Config\ProjectYamlLoader; use TypePhp\Build\ResourceCompilationTrait; @@ -39,7 +40,6 @@ use TypePhp\Resolver\ClassConstantValueTrait; use TypePhp\Transform\Visitor; use PhpParser\Modifiers; use PhpParser\Node; -use PhpParser\Node\Stmt\Foreach_; use PhpParser\NodeAbstract; use PhpParser\NodeTraverser; @@ -1219,6 +1219,8 @@ CODE; $sourceFiles[] = $this->getPhpxDir() . '/src/misc/ps_title.c'; } + $this->preparePhpXPrecompiledHeader(); + // Windows 平台:编译资源文件(图标、版本信息等) $this->compileResourceFile(); @@ -1230,6 +1232,46 @@ CODE; return $this->compileWithPcntl($sourceFiles, $job); } + protected function preparePhpXPrecompiledHeader(): void + { + $backend = $this->getCompilerBackend(); + if (!$backend->supportsPrecompiledHeaders()) { + return; + } + + $phpxDir = $this->getPhpxDir(); + $phpDir = $this->getPhpDir(); + $dependencies = [ + $phpxDir . '/include', + $phpxDir . '/src/misc', + $phpxDir . '/thirdparty/mpdecimal/libmpdec', + $phpxDir . '/thirdparty/mpdecimal/libmpdec++', + $phpDir . '/include', + ]; + + try { + $result = (new PrecompiledHeaderManager($backend, $this->getNativeBuilder()))->prepare( + $this->globalHeaders, + $dependencies, + $this->getBuildDir() . '/cache/pch', + $this->getCompileCommandOptions(), + ); + $this->precompiledHeader = [ + 'header' => $result['header'], + 'artifact' => $result['artifact'], + ]; + $displayArtifact = $this->getRelativePath($result['artifact']); + $this->climate->darkGray($result['cached'] + ? '[pch] cache: ' . $displayArtifact + : '[pch] built: ' . $displayArtifact); + } catch (\Throwable $e) { + // PCH is an optimization. A compiler-specific failure must not make + // an otherwise valid TypePHP project unbuildable. + $this->precompiledHeader = null; + $this->climate->warning('[pch] disabled: ' . $e->getMessage()); + } + } + protected function compileSourceFile(array $sourceFiles): array { $objectFiles = []; @@ -1358,7 +1400,9 @@ CODE; $this->climate->red("Compilation failed: {$source} ({$this->getCompileChildFailureReason($status)})"); } if ($this->noProgress) { - $percent = (int) ($completed / $totalFiles * 100); + $percent = $completed >= $totalFiles + ? 100 + : min(99, (int) ceil($completed / $totalFiles * 100)); $shortSource = $this->removeCommonPrefix($this->buildDir, $source); $this->climate->white("[{$completed}/{$totalFiles}] {$percent}% {$shortSource}"); } else { @@ -3573,82 +3617,6 @@ CODE; $existing->default === $incoming->default; } - protected function parseForeachObject(Foreach_ $node, ?string $objectExpr = null): string - { - $obj = $objectExpr ?? $this->parseIdentifier($node->expr); - $iterableVar = $this->genTmpVarName(); - $this->addLocalVar($iterableVar, Type::VAR); - - $iteratorObj = $this->genTmpVarName(); - $this->addLocalVar($iteratorObj, Type::OBJECT); - - $aggregateObj = $this->genTmpVarName(); - $this->addLocalVar($aggregateObj, Type::OBJECT); - - $tmpArrayVar = $this->genTmpVarName(); - $this->addLocalVar($tmpArrayVar, Type::ARRAY); - - $IteratorAggregateCe = $this->getClassEntryPtr('IteratorAggregate'); - $IteratorCe = $this->getClassEntryPtr('Iterator'); - $getIteratorStr = $this->getLiteralString('getIterator'); - $validStr = $this->getLiteralString('valid'); - $currentStr = $this->getLiteralString('current'); - $keyStr = $this->getLiteralString('key'); - $nextStr = $this->getLiteralString('next'); - $rewindStr = $this->getLiteralString('rewind'); - $invalidAggregateReturn = static function (string $aggregateObj): string { - return 'php::throwExceptionEx(zend_ce_exception, 0, ' - . '"Objects returned by %s::getIterator() must be traversable or implement interface Iterator", ' - . $aggregateObj . '.getClassName().toCString());'; - }; - - $code = $iterableVar . ' = ' . $obj . ';' . PHP_EOL; - $code .= $iteratorObj . ' = ' . $iterableVar . ';' . PHP_EOL; - $code .= 'if (' . $iteratorObj . '.instanceOf(' . $IteratorAggregateCe . ')) {' . PHP_EOL; - $this->indentLevel++; - $code .= $this->getIndent() . 'do {' . PHP_EOL; - $this->indentLevel++; - $code .= $this->getIndent() . $aggregateObj . ' = ' . $iteratorObj . ';' . PHP_EOL; - $code .= $this->getIndent() . $iterableVar . ' = ' . $aggregateObj . '.call(' . $getIteratorStr . ');' . PHP_EOL; - $code .= $this->getIndent() . 'if (UNEXPECTED(!' . $iterableVar . '.isObject())) {' . PHP_EOL; - $this->indentLevel++; - $code .= $this->getIndent() . $invalidAggregateReturn($aggregateObj) . PHP_EOL; - $this->indentLevel--; - $code .= $this->getIndent() . '}' . PHP_EOL; - $code .= $this->getIndent() . $iteratorObj . ' = ' . $iterableVar . ';' . PHP_EOL; - $code .= $this->getIndent() . 'if (UNEXPECTED(!' . $iteratorObj . '.instanceOf(' . $IteratorCe . ') && !' . $iteratorObj . '.instanceOf(' . $IteratorAggregateCe . '))) {' . PHP_EOL; - $this->indentLevel++; - $code .= $this->getIndent() . $invalidAggregateReturn($aggregateObj) . PHP_EOL; - $this->indentLevel--; - $code .= $this->getIndent() . '}' . PHP_EOL; - $this->indentLevel--; - $code .= $this->getIndent() . '} while (' . $iteratorObj . '.instanceOf(' . $IteratorAggregateCe . '));' . PHP_EOL; - $this->indentLevel--; - $code .= $this->getIndent() . '}' . PHP_EOL; - - $code .= 'if (' . $iteratorObj . '.instanceOf(' . $IteratorCe . ')) {' . PHP_EOL; - - $this->indentLevel++; - $code .= $this->getIndent() . $iteratorObj . '.call(' . $rewindStr . ');' . PHP_EOL; - $code .= $this->getIndent() . 'for (;' . $iteratorObj . '.call(' . $validStr . '); ' . $iteratorObj . '.call(' . $nextStr . ')) {' . PHP_EOL; - $this->indentLevel++; - - $code .= $this->parseForeachKeyAssignment($node, $iteratorObj . '.call(' . $keyStr . ')'); - $code .= $this->parseForeachValueAssignment($node, $iteratorObj . '.call(' . $currentStr . ')'); - $code .= $this->parseForeachBody($node); - $code .= '}' . PHP_EOL; - $this->indentLevel--; - $code .= $this->getIndent() . '} else {' . PHP_EOL; - $this->indentLevel++; - $code .= $this->getIndent() . $tmpArrayVar . ' = php::call(' . $this->getFuncPtr('get_object_vars') . ', {' . $obj . '});' . PHP_EOL; - $code .= $this->parseForeachArray($node, $tmpArrayVar); - $this->indentLevel--; - $this->indentLevel--; - $code .= $this->getIndent() . '}' . PHP_EOL; - - return $code; - } - private function getRegisterClassFunctionArgDef(ClassDef|InterfaceDef $classDef): string { $depsCeList = $this->getRegisterClassFunctionCeList($classDef); diff --git a/tests/compiler/loop/foreach-unified-iterable.phpt b/tests/compiler/loop/foreach-unified-iterable.phpt new file mode 100644 index 00000000..924d535f --- /dev/null +++ b/tests/compiler/loop/foreach-unified-iterable.phpt @@ -0,0 +1,46 @@ +--TEST-- +foreach uses one loop body for dynamically typed arrays and objects +--FILE-- + $group) { + foreach ($group as $itemName => $value) { + $result[] = "$groupName:$itemName=$value"; + } + } + return $result; +} + +function main(): void +{ + var_dump(flatten([ + 'array' => ['first' => 1, 'second' => 2], + ])); + + var_dump(flatten(new ArrayIterator([ + 'iterator' => new ArrayIterator(['third' => 3]), + ]))); + + $plain = new stdClass(); + $plain->object = (object) ['fourth' => 4]; + var_dump(flatten($plain)); +} +?> +--EXPECT-- +array(2) { + [0]=> + string(13) "array:first=1" + [1]=> + string(14) "array:second=2" +} +array(1) { + [0]=> + string(16) "iterator:third=3" +} +array(1) { + [0]=> + string(15) "object:fourth=4" +}