diff --git a/phpunit/code/count-literal-fold-safe.php b/phpunit/code/count-literal-fold-safe.php new file mode 100644 index 00000000..66c558c0 --- /dev/null +++ b/phpunit/code/count-literal-fold-safe.php @@ -0,0 +1,10 @@ + 1, 'a' => 2]), "\n"; + echo count([...$rest, 9]), "\n"; + echo count([$i++, $i++]), "\n"; +} diff --git a/phpunit/src/CountLiteralFoldTest.php b/phpunit/src/CountLiteralFoldTest.php new file mode 100644 index 00000000..83785291 --- /dev/null +++ b/phpunit/src/CountLiteralFoldTest.php @@ -0,0 +1,40 @@ +compileToCpp('count-literal-fold-unsafe.php'); + + // Element side effects, a repeated key and a spread each make the + // number of AST items differ from the runtime element count. + self::assertSame(4, substr_count($cpp, 'php::fn::count(')); + self::assertStringContainsString('php_bump()', $cpp); + self::assertStringContainsString('i++', $cpp); + } + + public function testPlainArrayLiteralsStillFoldAtCompileTime(): void + { + $cpp = $this->compileToCpp('count-literal-fold-safe.php'); + + self::assertStringNotContainsString('php::fn::count(', $cpp); + } + + private function compileToCpp(string $file): string + { + global $translator; + + $compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); + $translator = $compiler; + $source = TYPEPHP_ROOT_PATH . '/phpunit/code/' . $file; + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + + return file_get_contents($compiler->convertFile($source)); + } +} diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index fe2ae690..3c15ca65 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -656,11 +656,56 @@ trait FuncCallOptimizer } $arg = $expr->args[0]->value; if ($arg instanceof Node\Expr\Array_) { + if (!$this->isCountFoldableArray($arg)) { + return false; + } return count($arg->items) . $this->getPlatform()->getIntegerLiteralSuffix(); } return $this->genStdContainerCount($arg); } + /** + * The number of AST items only equals the runtime element count when no + * item spreads another array, no key can collide with another key, and + * dropping the element expressions cannot lose an observable effect. + * Anything else keeps the runtime php::fn::count() call. + */ + protected function isCountFoldableArray(Node\Expr\Array_ $array): bool + { + foreach ($array->items as $item) { + // [...$other] contributes an element count only known at runtime, + // and a key may collapse onto an earlier one: ['a' => 1, 'a' => 2] + // counts as one element, not two. + if ($item->unpack || $item->key !== null) { + return false; + } + if (!$this->isCountFoldableItem($item->value)) { + return false; + } + } + return true; + } + + protected function isCountFoldableItem(Node\Expr $value): bool + { + // ConstFetch also covers true, false and null. + if ($this->isScalar($value) + || $value instanceof Node\Expr\ConstFetch + || $value instanceof Node\Expr\ClassConstFetch + ) { + return true; + } + if ($value instanceof Node\Expr\UnaryMinus || $value instanceof Node\Expr\UnaryPlus) { + return $this->isCountFoldableItem($value->expr); + } + if ($value instanceof Node\Expr\Array_) { + return $this->isCountFoldableArray($value); + } + // A defined variable is a plain read; an undefined one must reach the + // dynamic path so it still reports the same diagnostic as PHP. + return $this->isVarExpr($value) && is_string($value->name) && $this->hasVar($value->name); + } + protected function doFoldKnownClass(Node\Expr\FuncCall $expr): string|false { $cn = $expr->args[0]->value; diff --git a/tests/compiler/array/count-literal-fold.phpt b/tests/compiler/array/count-literal-fold.phpt new file mode 100644 index 00000000..ade723fc --- /dev/null +++ b/tests/compiler/array/count-literal-fold.phpt @@ -0,0 +1,47 @@ +--TEST-- +count() on an array literal keeps spreads, duplicate keys and element side effects +--FILE-- + 1, 'a' => 2])); + + // A spread contributes a count only known at runtime. + $rest = [1, 2, 3, 4, 5]; + var_dump(count([...$rest, 9])); + + // Side effects of the elements must be observable afterwards. + $i = 0; + var_dump(count([$i++, $i++])); + var_dump($i); + + // Plain literals stay eligible for the compile-time fold. + $a = 1; + var_dump(count([1, 2, 3])); + var_dump(count([[1, 2], [3]])); + var_dump(count([$a, -2, true, null])); + var_dump(count([])); +} +?> +--EXPECT-- +bump +bump +int(2) +int(1) +int(6) +int(2) +int(2) +int(3) +int(2) +int(4) +int(0)