diff --git a/.github/actions/unix-arm64-build/action.yml b/.github/actions/unix-arm64-build/action.yml index ebdfe575..a4a3af3d 100644 --- a/.github/actions/unix-arm64-build/action.yml +++ b/.github/actions/unix-arm64-build/action.yml @@ -24,6 +24,13 @@ inputs: runs: using: composite steps: + - name: Checkout PHPX master + uses: actions/checkout@v4 + with: + repository: swoole/phpx + ref: master + path: third_party/phpx + - name: Setup PHP uses: shivammathur/setup-php@v2 with: diff --git a/.github/smoke/full-static/main.php b/.github/smoke/full-static/main.php new file mode 100644 index 00000000..1aa385b6 --- /dev/null +++ b/.github/smoke/full-static/main.php @@ -0,0 +1,43 @@ + true]) === '{"ok":true}', 'json_encode() failed'); + + $sum = 0; + for ($i = 0; $i < 100000; $i++) { + $sum += $i; + } + requireFullStatic($sum === 4999950000, 'loop result mismatch'); + + echo "full-static-smoke-ok\n"; +} diff --git a/.github/smoke/full-static/project.yml b/.github/smoke/full-static/project.yml new file mode 100644 index 00000000..5ca039c7 --- /dev/null +++ b/.github/smoke/full-static/project.yml @@ -0,0 +1,8 @@ +name: full-static-smoke +mode: bin +build-dir: build +output: full_static_smoke +cxx-std: c++17 + +sources: + - main.php diff --git a/.github/workflows/linux-arm64.yml b/.github/workflows/linux-arm64.yml index be702a7f..ceb602aa 100644 --- a/.github/workflows/linux-arm64.yml +++ b/.github/workflows/linux-arm64.yml @@ -26,7 +26,7 @@ jobs: matrix: php: ["8.4", "8.5"] env: - PHPX_HOME: ${{ github.workspace }}/vendor/swoole/phpx + PHPX_HOME: ${{ github.workspace }}/third_party/phpx steps: - name: Checkout TypePHP diff --git a/.github/workflows/linux-x64.yml b/.github/workflows/linux-x64.yml index 678c57b0..52b765b8 100644 --- a/.github/workflows/linux-x64.yml +++ b/.github/workflows/linux-x64.yml @@ -27,12 +27,19 @@ jobs: matrix: php: ["8.4", "8.5"] env: - PHPX_HOME: ${{ github.workspace }}/vendor/swoole/phpx + PHPX_HOME: ${{ github.workspace }}/third_party/phpx steps: - name: Checkout TypePHP uses: actions/checkout@v4 + - name: Checkout PHPX master + uses: actions/checkout@v4 + with: + repository: swoole/phpx + ref: master + path: third_party/phpx + - name: Checkout phpy uses: actions/checkout@v4 with: @@ -139,7 +146,11 @@ jobs: retention-days: 7 path: | tpc - vendor/swoole/phpx/lib/libphpx.so + third_party/phpx/lib/libphpx.so + third_party/phpx/include/** + third_party/phpx/src/misc/** + third_party/phpx/thirdparty/mpdecimal/libmpdec/** + third_party/phpx/thirdparty/mpdecimal/libmpdec++/** third_party/phpy/modules/phpy.so phpunit: @@ -153,7 +164,7 @@ jobs: matrix: php: ["8.4", "8.5"] env: - PHPX_HOME: ${{ github.workspace }}/vendor/swoole/phpx + PHPX_HOME: ${{ github.workspace }}/third_party/phpx steps: - name: Checkout TypePHP @@ -242,7 +253,7 @@ jobs: matrix: php: ["8.4", "8.5"] env: - PHPX_HOME: ${{ github.workspace }}/vendor/swoole/phpx + PHPX_HOME: ${{ github.workspace }}/third_party/phpx steps: - name: Checkout TypePHP @@ -349,7 +360,7 @@ jobs: matrix: php: ["8.4", "8.5"] env: - PHPX_HOME: ${{ github.workspace }}/vendor/swoole/phpx + PHPX_HOME: ${{ github.workspace }}/third_party/phpx NO_INTERACTION: 1 REPORT_EXIT_STATUS: 1 TYPEPHP_PHPT_GENERATED_ARTIFACT_DIR: ${{ github.workspace }}/build/phpt-generated diff --git a/.github/workflows/macos-arm64.yml b/.github/workflows/macos-arm64.yml index 4e0ea0c3..b03fde5d 100644 --- a/.github/workflows/macos-arm64.yml +++ b/.github/workflows/macos-arm64.yml @@ -26,7 +26,7 @@ jobs: matrix: php: ["8.4", "8.5"] env: - PHPX_HOME: ${{ github.workspace }}/vendor/swoole/phpx + PHPX_HOME: ${{ github.workspace }}/third_party/phpx steps: - name: Checkout TypePHP diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 7fcc6f4c..9f364472 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -30,6 +30,13 @@ jobs: - name: Checkout TypePHP uses: actions/checkout@v4 + - name: Checkout PHPX master + uses: actions/checkout@v4 + with: + repository: swoole/phpx + ref: master + path: third_party/phpx + - name: Setup PHP uses: shivammathur/setup-php@v2 with: @@ -75,7 +82,7 @@ jobs: "PHP_VERSION=$phpVersion" | Out-File $env:GITHUB_ENV -Append -Encoding utf8 "PHP_THREAD_SAFETY=$threadSafety" | Out-File $env:GITHUB_ENV -Append -Encoding utf8 "PHP_HOME=$phpHome" | Out-File $env:GITHUB_ENV -Append -Encoding utf8 - "PHPX_HOME=${{ github.workspace }}\vendor\swoole\phpx" | + "PHPX_HOME=${{ github.workspace }}\third_party\phpx" | Out-File $env:GITHUB_ENV -Append -Encoding utf8 "PHP_DEVEL_ARCHIVE=$archive" | Out-File $env:GITHUB_ENV -Append -Encoding utf8 "version=$phpVersion" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8 @@ -254,7 +261,7 @@ jobs: uses: actions/cache/restore@v4 with: path: ${{ env.PHPX_HOME }}\thirdparty\mpdecimal\vcbuild\dist64 - key: windows-2022-msvc-mpdecimal-x64-${{ hashFiles('vendor/swoole/phpx/thirdparty/mpdecimal/**') }} + key: windows-2022-msvc-mpdecimal-x64-${{ hashFiles('third_party/phpx/thirdparty/mpdecimal/**') }} - name: Build mpdecimal if: steps.cache-mpdecimal.outputs.cache-hit != 'true' diff --git a/benchmark/dynamic-call/README.md b/benchmark/dynamic-call/README.md index ddf27334..7c506365 100644 --- a/benchmark/dynamic-call/README.md +++ b/benchmark/dynamic-call/README.md @@ -10,6 +10,10 @@ It also measures dynamic method names with a stable receiver, alternating method names, and a fixed method name on changing receiver classes. Those cases require a class-entry guard in addition to a callable-name guard. +The monomorphic string-call cases cover zero, one, two, and four positional +arguments. This separates callable-cache lookup cost from argument +materialization cost and protects the small stack-argument fast path. + Run it from the repository root against a release PHP/PHPX build: ```bash diff --git a/benchmark/dynamic-call/benchmark.php b/benchmark/dynamic-call/benchmark.php index 4261b68c..9741f207 100644 --- a/benchmark/dynamic-call/benchmark.php +++ b/benchmark/dynamic-call/benchmark.php @@ -5,11 +5,26 @@ declare(strict_types=1); const DYNAMIC_CALL_ITERATIONS = 1_000_000; const DYNAMIC_CALL_ROUNDS = 5; +function dynamicCallNoArgs(): int +{ + return 1; +} + function dynamicCallAddOne(int $value): int { return $value + 1; } +function dynamicCallAddTwoArgs(int $left, int $right): int +{ + return $left + $right; +} + +function dynamicCallAddFourArgs(int $a, int $b, int $c, int $d): int +{ + return $a + $b + $c + $d; +} + function dynamicCallAddTwo(int $value): int { return $value + 2; @@ -100,6 +115,36 @@ function runMonomorphicStringCall(int $iterations): int return $sum; } +function runMonomorphicStringCallZeroArgs(int $iterations): int +{ + $callback = 'dynamicCallNoArgs'; + $sum = 0; + for ($i = 0; $i < $iterations; $i++) { + $sum += $callback(); + } + return $sum; +} + +function runMonomorphicStringCallTwoArgs(int $iterations): int +{ + $callback = 'dynamicCallAddTwoArgs'; + $sum = 0; + for ($i = 0; $i < $iterations; $i++) { + $sum += $callback($i, 1); + } + return $sum; +} + +function runMonomorphicStringCallFourArgs(int $iterations): int +{ + $callback = 'dynamicCallAddFourArgs'; + $sum = 0; + for ($i = 0; $i < $iterations; $i++) { + $sum += $callback($i, 1, 2, 3); + } + return $sum; +} + function runAlternatingStringCall(int $iterations): int { $sum = 0; @@ -221,7 +266,10 @@ function runDynamicCallCase(string $case, int $iterations): int { return match ($case) { 'direct' => runDirectCall($iterations), + 'string_monomorphic_zero' => runMonomorphicStringCallZeroArgs($iterations), 'string_monomorphic' => runMonomorphicStringCall($iterations), + 'string_monomorphic_two' => runMonomorphicStringCallTwoArgs($iterations), + 'string_monomorphic_four' => runMonomorphicStringCallFourArgs($iterations), 'string_alternating' => runAlternatingStringCall($iterations), 'string_megamorphic' => runMegamorphicStringCall($iterations), 'closure_monomorphic' => runMonomorphicClosureCall($iterations), @@ -262,7 +310,10 @@ function main(): void $selectedCase = getenv('DYNAMIC_CALL_CASE'); foreach ([ 'direct', + 'string_monomorphic_zero', 'string_monomorphic', + 'string_monomorphic_two', + 'string_monomorphic_four', 'string_alternating', 'string_megamorphic', 'closure_monomorphic', diff --git a/benchmark/dynamic-call/run.php b/benchmark/dynamic-call/run.php index d98c6092..e6691f83 100644 --- a/benchmark/dynamic-call/run.php +++ b/benchmark/dynamic-call/run.php @@ -126,7 +126,10 @@ $typephp = parseDynamicCallResults(runDynamicCallCommand([$binary], $root, true, $cases = [ 'direct', + 'string_monomorphic_zero', 'string_monomorphic', + 'string_monomorphic_two', + 'string_monomorphic_four', 'string_alternating', 'string_megamorphic', 'closure_monomorphic', diff --git a/benchmark/property-access/README.md b/benchmark/property-access/README.md index 488dc99e..db81da6f 100644 --- a/benchmark/property-access/README.md +++ b/benchmark/property-access/README.md @@ -13,4 +13,6 @@ php benchmark/property-access/run.php To reuse an existing binary, add `--skip-build`. For local regression checks, `--max-ratio=1.5` exits unsuccessfully when a dynamic read or write takes more -than 1.5 times the corresponding Zend PHP result. +than 1.5 times the corresponding Zend PHP result. Use +`--case=dynamic_write`, `--case=dynamic_read`, `--case=static_write`, or +`--case=static_read` to isolate one workload while profiling. diff --git a/benchmark/property-access/benchmark.php b/benchmark/property-access/benchmark.php index 8c774c53..588fc903 100644 --- a/benchmark/property-access/benchmark.php +++ b/benchmark/property-access/benchmark.php @@ -107,6 +107,7 @@ function measure(callable $callback, int $operations): float function main(): void { global $benchmarkSink; + $selectedCase = getenv('PROPERTY_ACCESS_CASE'); $benchmarkSink = 0; $iterations = 200000; $data = [ @@ -121,34 +122,41 @@ function main(): void $static = new StaticPropertyEntity(); $operations = $iterations * 5; - $dynamicWrite = measure( - function () use ($dynamic, $data, $iterations): int { - return runDynamicWrite($dynamic, $data, $iterations); - }, - $operations, - ); - $staticWrite = measure( - function () use ($static, $data, $iterations): int { - return runStaticWrite($static, $data, $iterations); - }, - $operations, - ); - $dynamicRead = measure( - function () use ($dynamic, $properties, $iterations): int { - return runDynamicRead($dynamic, $properties, $iterations); - }, - $operations, - ); - $staticRead = measure( - function () use ($static, $iterations): int { - return runStaticRead($static, $iterations); - }, - $operations, - ); - - printf("dynamic_write_ns=%.3f\n", $dynamicWrite); - printf("static_write_ns=%.3f\n", $staticWrite); - printf("dynamic_read_ns=%.3f\n", $dynamicRead); - printf("static_read_ns=%.3f\n", $staticRead); + if (!is_string($selectedCase) || $selectedCase === '' || $selectedCase === 'dynamic_write') { + $dynamicWrite = measure( + function () use ($dynamic, $data, $iterations): int { + return runDynamicWrite($dynamic, $data, $iterations); + }, + $operations, + ); + printf("dynamic_write_ns=%.3f\n", $dynamicWrite); + } + if (!is_string($selectedCase) || $selectedCase === '' || $selectedCase === 'static_write') { + $staticWrite = measure( + function () use ($static, $data, $iterations): int { + return runStaticWrite($static, $data, $iterations); + }, + $operations, + ); + printf("static_write_ns=%.3f\n", $staticWrite); + } + if (!is_string($selectedCase) || $selectedCase === '' || $selectedCase === 'dynamic_read') { + $dynamicRead = measure( + function () use ($dynamic, $properties, $iterations): int { + return runDynamicRead($dynamic, $properties, $iterations); + }, + $operations, + ); + printf("dynamic_read_ns=%.3f\n", $dynamicRead); + } + if (!is_string($selectedCase) || $selectedCase === '' || $selectedCase === 'static_read') { + $staticRead = measure( + function () use ($static, $iterations): int { + return runStaticRead($static, $iterations); + }, + $operations, + ); + printf("static_read_ns=%.3f\n", $staticRead); + } echo 'checksum=', $benchmarkSink + $dynamic->sum($properties) + $static->sum(), "\n"; } diff --git a/benchmark/property-access/run.php b/benchmark/property-access/run.php index 5776accd..71fb41fa 100644 --- a/benchmark/property-access/run.php +++ b/benchmark/property-access/run.php @@ -8,10 +8,14 @@ $project = __DIR__ . '/project.yml'; $binary = __DIR__ . '/property_access'; $skipBuild = in_array('--skip-build', $argv, true); $maximumRatio = null; +$selectedCase = null; foreach ($argv as $argument) { if (str_starts_with($argument, '--max-ratio=')) { $maximumRatio = (float) substr($argument, strlen('--max-ratio=')); } + if (str_starts_with($argument, '--case=')) { + $selectedCase = substr($argument, strlen('--case=')); + } } /** @@ -82,20 +86,23 @@ if (!is_file($binary)) { throw new RuntimeException('Benchmark binary does not exist: ' . $binary); } +$benchmarkEnvironment = getenv(); +if ($selectedCase !== null && $selectedCase !== '') { + $benchmarkEnvironment['PROPERTY_ACCESS_CASE'] = $selectedCase; +} $php = parseResults(runCommand([ PHP_BINARY, '-d', 'opcache.enable_cli=0', '-r', 'require ' . var_export($source, true) . '; main();', -], $root, true)); -$typephpEnvironment = null; +], $root, true, $benchmarkEnvironment)); +$typephpEnvironment = $benchmarkEnvironment; if (PHP_OS_FAMILY !== 'Windows') { $phpxHome = getenv('PHPX_HOME'); if (!is_string($phpxHome) || $phpxHome === '') { $phpxHome = $root . '/vendor/swoole/phpx'; } - $typephpEnvironment = getenv(); $loaderVariable = PHP_OS_FAMILY === 'Darwin' ? 'DYLD_LIBRARY_PATH' : 'LD_LIBRARY_PATH'; $existingPath = $typephpEnvironment[$loaderVariable] ?? ''; $typephpEnvironment[$loaderVariable] = $phpxHome . '/lib' @@ -106,7 +113,11 @@ $typephp = parseResults(runCommand([$binary], $root, true, $typephpEnvironment)) echo "Metric PHP ns/op TypePHP ns/op TypePHP/PHP\n"; echo "------------------------------------------------------------\n"; $failed = false; -foreach (['dynamic_write_ns', 'dynamic_read_ns', 'static_write_ns', 'static_read_ns'] as $metric) { +$metrics = ['dynamic_write_ns', 'dynamic_read_ns', 'static_write_ns', 'static_read_ns']; +if ($selectedCase !== null && $selectedCase !== '') { + $metrics = [$selectedCase . '_ns']; +} +foreach ($metrics as $metric) { if (!isset($php[$metric], $typephp[$metric])) { throw new RuntimeException('Missing benchmark metric: ' . $metric); } diff --git a/phpunit/src/GeneratedCodeIndentationTest.php b/phpunit/src/GeneratedCodeIndentationTest.php index f665c003..d85535c9 100644 --- a/phpunit/src/GeneratedCodeIndentationTest.php +++ b/phpunit/src/GeneratedCodeIndentationTest.php @@ -18,7 +18,7 @@ class GeneratedCodeIndentationTest extends \PHPUnit\Framework\TestCase $code = file_get_contents($cppFile); $this->assertIsString($code); $this->assertStringContainsString( - "\t\twhile (tmp_var_0.next()) {\n\t\t\titem = tmp_var_0.value();", + "\t\twhile (tmp_var_0.nextValue(item)) {", $code, ); $this->assertStringContainsString( diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index 0f1c836f..e772e236 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -471,7 +471,9 @@ trait CallArgumentGenerator $this->context->beforeStmtLines[] = $namedArgsArray . '.set(' . $this->getLiteralString($arg->name->name) . ', ' . $value . ');'; } else { $arrayArgs = $this->ensureCallArrayArgs($arrayArgsVar, $list_args); - $this->context->beforeStmtLines[] = $arrayArgs . '.set(' . $this->getLiteralString($arg->name->name) . ', ' . $value . ');'; + $method = $forceArrayArgs ? 'setValue' : 'set'; + $this->context->beforeStmtLines[] = $arrayArgs . '.' . $method . '(' + . $this->getLiteralString($arg->name->name) . ', ' . $value . ');'; } continue; } @@ -495,13 +497,13 @@ trait CallArgumentGenerator } } $value = $this->wrapScopedCallbackArg($arg, $this->parseCallArgValue($arg)); - $this->addPositionalCallArg($value, $arrayArgsVar, $list_args); + $this->addPositionalCallArg($value, $arrayArgsVar, $list_args, $forceArrayArgs); continue; } if ($this->isVarExpr($arg->value)) { $name = $this->parseIdentifier($arg->value); if ($byRef) { - $this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args); + $this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args, $forceArrayArgs); continue; } if (!$this->hasVar($name)) { @@ -509,7 +511,7 @@ trait CallArgumentGenerator } } elseif ($this->isPropertyFetch($arg->value)) { if ($byRef) { - $this->addPositionalCallArg($this->emitDynamicPropertyFetchRef($arg->value, $arg), $arrayArgsVar, $list_args); + $this->addPositionalCallArg($this->emitDynamicPropertyFetchRef($arg->value, $arg), $arrayArgsVar, $list_args, $forceArrayArgs); continue; } if ($this->isVarExpr($arg->value->var)) { @@ -526,9 +528,9 @@ trait CallArgumentGenerator if ($byRef) { $ref = $this->addTmpVar(Type::REF); $this->context->beforeStmtLines[] = $ref . ' = ' . $globalVar . '.toReference();'; - $this->addPositionalCallArg('&' . $ref, $arrayArgsVar, $list_args); + $this->addPositionalCallArg('&' . $ref, $arrayArgsVar, $list_args, $forceArrayArgs); } else { - $this->addPositionalCallArg($globalVar, $arrayArgsVar, $list_args); + $this->addPositionalCallArg($globalVar, $arrayArgsVar, $list_args, $forceArrayArgs); } continue; } @@ -539,7 +541,7 @@ trait CallArgumentGenerator if ($arg->value->dim === null) { $this->fatalError($arg, 'Array dimension must be a constant expression'); } - $this->addPositionalCallArg($array . '.itemRef(' . $this->identifierToStr($arg->value->dim) . ')', $arrayArgsVar, $list_args); + $this->addPositionalCallArg($array . '.itemRef(' . $this->identifierToStr($arg->value->dim) . ')', $arrayArgsVar, $list_args, $forceArrayArgs); continue; } } elseif ($this->isReferenceWrapperCall($arg->value)) { @@ -547,12 +549,12 @@ trait CallArgumentGenerator if ($this->isVarExpr($inner)) { $name = $this->parseVariable($inner); $arg->value = $inner; - $this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args); + $this->addPositionalCallArg($this->parseArgRefVar($arg, $name), $arrayArgsVar, $list_args, $forceArrayArgs); continue; } $expr = $this->expandRefvalExpr($inner, $arg); if ($expr !== null) { - $this->addPositionalCallArg($expr, $arrayArgsVar, $list_args); + $this->addPositionalCallArg($expr, $arrayArgsVar, $list_args, $forceArrayArgs); continue; } $this->fatalError($arg, 'The refval function only accepts a variable, array element, or object property'); @@ -564,12 +566,12 @@ trait CallArgumentGenerator $tmpRef = $this->genTmpVarName(); $this->addLocalVar($tmpRef, Type::REF); $this->context->beforeStmtLines[] = $tmpRef . ' = ' . $this->parseChainedExpr($arg->value, self::OP_REFVAL) . ';'; - $this->addPositionalCallArg('&' . $tmpRef, $arrayArgsVar, $list_args); + $this->addPositionalCallArg('&' . $tmpRef, $arrayArgsVar, $list_args, $forceArrayArgs); continue; } } $value = $this->parseCallArgValue($arg); - $this->addPositionalCallArg($value, $arrayArgsVar, $list_args); + $this->addPositionalCallArg($value, $arrayArgsVar, $list_args, $forceArrayArgs); } if ($argsVar !== null) { @@ -635,10 +637,15 @@ trait CallArgumentGenerator return $namedArgsVar; } - protected function addPositionalCallArg(string $value, ?string $arrayArgsVar, array &$listArgs): void - { + protected function addPositionalCallArg( + string $value, + ?string $arrayArgsVar, + array &$listArgs, + bool $appendByValue = false, + ): void { if ($arrayArgsVar !== null) { - $this->context->beforeStmtLines[] = $arrayArgsVar . '.append(' . $value . ');'; + $method = $appendByValue ? 'appendValue' : 'append'; + $this->context->beforeStmtLines[] = $arrayArgsVar . '.' . $method . '(' . $value . ');'; } else { $listArgs[] = $value; } diff --git a/src/Parser/ForeachTrait.php b/src/Parser/ForeachTrait.php index 47f3fead..7fc63ec2 100644 --- a/src/Parser/ForeachTrait.php +++ b/src/Parser/ForeachTrait.php @@ -120,23 +120,69 @@ trait ForeachTrait return $this->getIndent() . $valueVar . ' = ' . $valueExpr . ';' . PHP_EOL; } - protected function parseForeachIterable(Foreach_ $node, string $iterableVar): string + /** + * Return a foreach target that can be filled directly by ForeachIterator. + * + * The combined iterator API deliberately accepts only ordinary Variant + * variables. References, native scalar locals, properties, array offsets, + * and destructuring retain the general assignment path below. + */ + protected function parseDirectForeachTarget(Foreach_ $node, Expr $target): ?string + { + if (!$this->isVarExpr($target)) { + return null; + } + + $name = $this->parseIdentifier($target); + if ($this->hasVar($name) && $this->getVarType($name) !== Type::VAR) { + return null; + } + $this->checkVar($node, $name); + return $name; + } + + protected function parseForeachIterable( + Foreach_ $node, + string $iterableVar, + bool $allowDirectArrayTargets = false, + ): string { $iterator = $this->genTmpVarName(); $byRef = $node->byRef ? 'true' : 'false'; $scope = $this->class ? $this->getLocalClassEntryPtr($this->getFullClassName()) : 'nullptr'; + $directValue = null; + $directKey = null; + if ($allowDirectArrayTargets && !$node->byRef) { + $directValue = $this->parseDirectForeachTarget($node, $node->valueVar); + if ($directValue !== null && $node->keyVar !== null) { + $directKey = $this->parseDirectForeachTarget($node, $node->keyVar); + if ($directKey === null) { + $directValue = null; + } + } + } + $code = '{' . PHP_EOL; $this->indentLevel++; $code .= $this->getIndent() . "php::ForeachIterator $iterator{{$iterableVar}, $byRef, $scope};" . PHP_EOL; - $code .= $this->getIndent() . "while ($iterator.next()) {" . PHP_EOL; + if ($directValue !== null) { + $next = $directKey === null + ? "$iterator.nextValue($directValue)" + : "$iterator.nextKeyValue($directKey, $directValue)"; + } else { + $next = "$iterator.next()"; + } + $code .= $this->getIndent() . "while ($next) {" . PHP_EOL; $this->indentLevel++; - $code .= $this->parseForeachKeyAssignment($node, $iterator . '.key()'); - $code .= $this->parseForeachValueAssignment( - $node, - $iterator . '.value()', - $iterator . '.assignValueRef', - ); + if ($directValue === null) { + $code .= $this->parseForeachKeyAssignment($node, $iterator . '.key()'); + $code .= $this->parseForeachValueAssignment( + $node, + $iterator . '.value()', + $iterator . '.assignValueRef', + ); + } $body = $this->parseForeachBody($node); $this->indentLevel--; @@ -203,7 +249,7 @@ trait ForeachTrait || $type === Type::OBJECT || ($node->byRef && ($type === Type::VAR || $type === Type::REF)) ) { - return $this->parseForeachIterable($node, $name); + return $this->parseForeachIterable($node, $name, $type === Type::ARRAY); } elseif ($this->isStdContainerType($type)) { return $this->parseForeachStdContainer($node); } diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index a1fc3f58..64a43b76 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -358,7 +358,11 @@ trait MethodCallTrait forceArrayArgs: true, ); - return self::PREFIX . $nativeFunc . '(' . $object . ', ' . $method . ', ' . $arguments . ')'; + // The argument array is a compiler-owned temporary used only by this + // direct call. Transfer its zval instead of incrementing/decrementing + // the array refcount at the generated __call() boundary. + return self::PREFIX . $nativeFunc . '(' + . $object . ', ' . $method . ', std::move(' . $arguments . '))'; } protected function parseNativeMethodCall(string $object, string $nativeFunc, array $args): string diff --git a/tests/compiler/dynamic_call/call-cache-arguments.phpt b/tests/compiler/dynamic_call/call-cache-arguments.phpt new file mode 100644 index 00000000..6194e8df --- /dev/null +++ b/tests/compiler/dynamic_call/call-cache-arguments.phpt @@ -0,0 +1,51 @@ +--TEST-- +Dynamic call cache preserves small, large, named, unpacked, reference, and exception arguments +--FILE-- +getMessage(), "\n"; + } +} +?> +--EXPECT-- +int(10) +int(15) +int(10) +int(15) +int(11) +int(11) +cached failure diff --git a/tests/compiler/dynamic_call/call-magic-direct-safe.phpt b/tests/compiler/dynamic_call/call-magic-direct-safe.phpt index cc26502c..6af2f38b 100644 --- a/tests/compiler/dynamic_call/call-magic-direct-safe.phpt +++ b/tests/compiler/dynamic_call/call-magic-direct-safe.phpt @@ -22,6 +22,15 @@ class RuntimeMethod extends DirectMagic } } +final class MutatingMagic +{ + public function __call(string $name, array $arguments): mixed + { + $arguments[0] = 'changed-in-call'; + return $arguments; + } +} + function callFromDeclaredBase(DirectMagic $object): mixed { return $object->existing(); @@ -39,6 +48,14 @@ function main(): void // A declared base type is not an exact runtime type. The compiler must // retain Zend dispatch so a subclass's real method wins over __call(). var_dump(callFromDeclaredBase(new RuntimeMethod())); + + // Zend constructs __call()'s argument array by value. A source reference + // must not leak into that array, including on the direct compiled path. + $source = 'original'; + $reference = &$source; + $mutating = new MutatingMagic(); + var_dump($mutating->missing($reference)); + var_dump($source); } ?> --EXPECT-- @@ -67,3 +84,8 @@ array(2) { } } string(14) "runtime-method" +array(1) { + [0]=> + string(15) "changed-in-call" +} +string(8) "original" diff --git a/tests/compiler/loop/foreach-direct-array-targets.phpt b/tests/compiler/loop/foreach-direct-array-targets.phpt new file mode 100644 index 00000000..d7382292 --- /dev/null +++ b/tests/compiler/loop/foreach-direct-array-targets.phpt @@ -0,0 +1,77 @@ +--TEST-- +foreach direct array targets preserve keys, values, references, and assignment order +--FILE-- + $value) { + $result[] = $key . ':' . $value; + } + return $result; +} + +function valuesOnly(array $values): array +{ + $result = []; + foreach ($values as $value) { + $result[] = $value; + } + return $result; +} + +function sameTarget(array $values): mixed +{ + $item = null; + foreach ($values as $item => $item) { + } + return $item; +} + +function main(): void +{ + var_dump(collect([2 => 'two', 'name' => 'value'])); + var_dump(valuesOnly([10, 20])); + var_dump(sameTarget([7 => 'last-value'])); + + $source = 10; + $values = [&$source]; + foreach ($values as $value) { + $value = 99; + } + var_dump($source, $value); + + $snapshot = ['a' => 1, 'b' => 2]; + $seen = []; + foreach ($snapshot as $key => $value) { + $seen[] = $key . ':' . $value; + unset($snapshot[$key]); + } + var_dump($seen, $snapshot); +} +?> +--EXPECT-- +array(2) { + [0]=> + string(5) "2:two" + [1]=> + string(10) "name:value" +} +array(2) { + [0]=> + int(10) + [1]=> + int(20) +} +string(10) "last-value" +int(10) +int(99) +array(2) { + [0]=> + string(3) "a:1" + [1]=> + string(3) "b:2" +} +array(0) { +} diff --git a/tests/compiler/object_property/dynamic-property-string-name.phpt b/tests/compiler/object_property/dynamic-property-string-name.phpt new file mode 100644 index 00000000..c9df8c18 --- /dev/null +++ b/tests/compiler/object_property/dynamic-property-string-name.phpt @@ -0,0 +1,48 @@ +--TEST-- +dynamic property access preserves string, referenced-string, and converted names +--FILE-- +values[$name] ?? null; + } + + public function __set(string $name, mixed $value): void + { + $this->values[$name] = $value; + } +} + +function readName(DynamicNameBag $bag, mixed $name): mixed +{ + return $bag->{$name}; +} + +function writeName(DynamicNameBag $bag, mixed $name, mixed $value): void +{ + $bag->{$name} = $value; +} + +function main(): void +{ + $bag = new DynamicNameBag(); + $name = 'answer'; + writeName($bag, $name, 42); + var_dump(readName($bag, $name)); + + $alias = &$name; + var_dump(readName($bag, $alias)); + + writeName($bag, 7, 'seven'); + var_dump(readName($bag, 7)); +} +?> +--EXPECT-- +int(42) +int(42) +string(5) "seven"