feat(parser): refactor string concatenation and foreach iteration handling

- Replace direct php::concat calls with flattened concatenation using parseFlattenedConcat
- Add prepareConcatOperand method to handle proper string conversion for different types
- Implement unified foreach iterator using php::ForeachIterator for both arrays and objects
- Remove separate parseForeachObject method and consolidate iteration logic
- Add support for precompiled headers in GCC-like backends
- Implement PrecompiledHeaderManager for PCH preparation and caching
- Optimize build queue with SourceCompileQueue using largest-first sorting strategy
- Improve progress percentage calculation during compilation
- Add new test case for unified foreach iteration over different iterable types
pull/18/head
韩天峰 1 month ago
parent cf8bfdef06
commit aae4f2a07d
  1. 10
      src/Backend/Clang.php
  2. 10
      src/Backend/CompilerBackend.php
  3. 20
      src/Backend/GccLikeBackend.php
  4. 23
      src/Build/NativeBuilder.php
  5. 11
      src/Build/NativeCommandOptionsTrait.php
  6. 104
      src/Build/PrecompiledHeaderManager.php
  7. 22
      src/Build/SourceCompileQueue.php
  8. 34
      src/Parser/AssignOpTrait.php
  9. 26
      src/Parser/BinaryOpTrait.php
  10. 56
      src/Parser/ForeachTrait.php
  11. 6
      src/Parser/TypeConversionTrait.php
  12. 124
      src/Translator.php
  13. 46
      tests/compiler/loop/foreach-unified-iterable.phpt

@ -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) {

@ -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');
}
/**
* 编译单个文件
*/

@ -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
{

@ -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'];

@ -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

@ -0,0 +1,104 @@
<?php
namespace TypePhp\Build;
use TypePhp\Backend\CompilerBackend;
final readonly class PrecompiledHeaderManager
{
public function __construct(
private CompilerBackend $backend,
private NativeBuilder $builder,
) {
}
/**
* @param list<string> $headers
* @param list<string> $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<string> $headers @param list<string> $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);
}
}

@ -0,0 +1,22 @@
<?php
namespace TypePhp\Build;
final class SourceCompileQueue
{
/** @param list<string> $sources @return list<string> */
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');
}
}

@ -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 . ')';
}

@ -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);

@ -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) 来显式地使用原生类型
*/
}

@ -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 . ')';
}

@ -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);

@ -0,0 +1,46 @@
--TEST--
foreach uses one loop body for dynamically typed arrays and objects
--FILE--
<?php
function flatten($groups): array
{
$result = [];
foreach ($groups as $groupName => $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"
}
Loading…
Cancel
Save