Skip to content

Commit 6b6ce4f

Browse files
author
Giandonn
committed
fix(optimizer): restrict the count() literal fold to provably inert items
The first whitelist was too broad. ConstFetch, ClassConstFetch and the base Node\Scalar type all admit expressions PHP must still evaluate, so count([UNDEFINED_COUNT_LITERAL]), count([KnownClass::MISSING]) and count(["{$object->property}"]) folded to 1, dropping two Errors and a __get() call. The defined-variable check was not a purity proof either: hasVar() only reports a compiler slot, not that the variable is still initialized on every path after unset(). Narrow the fold to items whose evaluation cannot be observed: - literal Int_, Float_ and String_ (an interpolated string is a distinct InterpolatedString node, so String_ already excludes it); - the language constants true, false and null only; - unary plus/minus over a literal int or float; - recursively safe nested arrays. Variables, general constant and class constant fetches, interpolated strings and every other expression stay on the runtime path, and by-reference items are now rejected explicitly alongside keys and unpacking. Cover the three reported cases plus a by-reference item, a plain variable read and a defined class constant in both the fold-decision test and the PHPT.
1 parent 8328c4b commit 6b6ce4f

5 files changed

Lines changed: 103 additions & 20 deletions

File tree

phpunit/code/count-literal-fold-safe.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@
88

99
function main(): void
1010
{
11-
$a = 1;
12-
1311
echo count([1, 2, 3]), "\n";
1412
echo count([[1, 2], [3]]), "\n";
15-
echo count([$a, -2, true, null]), "\n";
13+
echo count([1.5, 'text', true, false, null]), "\n";
14+
echo count([-2, +3, -1.5]), "\n";
1615
echo count([]), "\n";
1716
}

phpunit/code/count-literal-fold-unsafe.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66
* @contact service@swoole.com
77
*/
88

9+
class KnownClass
10+
{
11+
public const KNOWN = 1;
12+
}
13+
14+
class MagicHolder
15+
{
16+
public function __get(string $name): int
17+
{
18+
echo "get-{$name}\n";
19+
return 1;
20+
}
21+
}
22+
923
function bump(): int
1024
{
1125
echo "bump\n";
@@ -16,9 +30,18 @@ function main(): void
1630
{
1731
$rest = [1, 2, 3, 4, 5];
1832
$i = 0;
33+
$plain = 1;
34+
$ref = 1;
35+
$object = new MagicHolder();
1936

2037
echo count([bump(), bump()]), "\n";
2138
echo count(['a' => 1, 'a' => 2]), "\n";
2239
echo count([...$rest, 9]), "\n";
2340
echo count([$i++, $i++]), "\n";
41+
echo count([$plain]), "\n";
42+
echo count([&$ref]), "\n";
43+
echo count([UNDEFINED_COUNT_LITERAL]), "\n";
44+
echo count([KnownClass::MISSING]), "\n";
45+
echo count(["{$object->property}"]), "\n";
46+
echo count([KnownClass::KNOWN]), "\n";
2447
}

phpunit/src/CountLiteralFoldTest.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ public function testUnfoldableArrayLiteralsKeepTheRuntimeCall(): void
2121
{
2222
$cpp = $this->compileToCpp('count-literal-fold-unsafe.php');
2323

24-
// Element side effects, a repeated key and a spread each make the
25-
// number of AST items differ from the runtime element count.
26-
self::assertSame(4, substr_count($cpp, 'php::fn::count('));
24+
// Every call in the fixture must stay on the runtime path: element
25+
// side effects, a repeated key, a spread, a by-reference item, a
26+
// plain variable read, a constant or class constant fetch that may
27+
// be undefined, and an interpolated string that may call __get().
28+
self::assertSame(10, substr_count($cpp, 'php::fn::count('));
2729
self::assertStringContainsString('php_bump()', $cpp);
2830
self::assertStringContainsString('i++', $cpp);
2931
}

src/Optimizer/FuncCallOptimizer.php

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -674,9 +674,10 @@ protected function isCountFoldableArray(Node\Expr\Array_ $array): bool
674674
{
675675
foreach ($array->items as $item) {
676676
// [...$other] contributes an element count only known at runtime,
677-
// and a key may collapse onto an earlier one: ['a' => 1, 'a' => 2]
678-
// counts as one element, not two.
679-
if ($item->unpack || $item->key !== null) {
677+
// a key may collapse onto an earlier one (['a' => 1, 'a' => 2]
678+
// counts as one element, not two), and a by-reference item binds
679+
// its source variable instead of reading it.
680+
if ($item->unpack || $item->key !== null || $item->byRef) {
680681
return false;
681682
}
682683
if (!$this->isCountFoldableItem($item->value)) {
@@ -686,24 +687,34 @@ protected function isCountFoldableArray(Node\Expr\Array_ $array): bool
686687
return true;
687688
}
688689

690+
/**
691+
* Only expressions whose evaluation is provably free of observable effects
692+
* may be discarded. Variables, general constant and class constant
693+
* fetches, interpolated strings and every other expression stay on the
694+
* runtime path: they can be undefined, autoload, throw or call __get().
695+
*/
689696
protected function isCountFoldableItem(Node\Expr $value): bool
690697
{
691-
// ConstFetch also covers true, false and null.
692-
if ($this->isScalar($value)
693-
|| $value instanceof Node\Expr\ConstFetch
694-
|| $value instanceof Node\Expr\ClassConstFetch
698+
// Node\Scalar\String_ is the literal string only; an interpolated
699+
// string is a distinct Node\Scalar\InterpolatedString node.
700+
if ($value instanceof Node\Scalar\Int_
701+
|| $value instanceof Node\Scalar\Float_
702+
|| $value instanceof Node\Scalar\String_
695703
) {
696704
return true;
697705
}
706+
// The language constants only. Any other name may be undefined and
707+
// must still raise the same Error PHP raises.
708+
if ($value instanceof Node\Expr\ConstFetch) {
709+
return in_array(strtolower($value->name->toString()), ['true', 'false', 'null'], true);
710+
}
698711
if ($value instanceof Node\Expr\UnaryMinus || $value instanceof Node\Expr\UnaryPlus) {
699-
return $this->isCountFoldableItem($value->expr);
712+
return $value->expr instanceof Node\Scalar\Int_ || $value->expr instanceof Node\Scalar\Float_;
700713
}
701714
if ($value instanceof Node\Expr\Array_) {
702715
return $this->isCountFoldableArray($value);
703716
}
704-
// A defined variable is a plain read; an undefined one must reach the
705-
// dynamic path so it still reports the same diagnostic as PHP.
706-
return $this->isVarExpr($value) && is_string($value->name) && $this->hasVar($value->name);
717+
return false;
707718
}
708719

709720
protected function doFoldKnownClass(Node\Expr\FuncCall $expr): string|false

tests/compiler/array/count-literal-fold.phpt

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22
count() on an array literal keeps spreads, duplicate keys and element side effects
33
--FILE--
44
<?php
5+
class KnownClass
6+
{
7+
public const KNOWN = 1;
8+
}
9+
10+
class MagicHolder
11+
{
12+
public function __get(string $name): int
13+
{
14+
echo "get-{$name}\n";
15+
return 1;
16+
}
17+
}
18+
519
function bump(): int
620
{
721
echo "bump\n";
@@ -25,11 +39,38 @@ function main()
2539
var_dump(count([$i++, $i++]));
2640
var_dump($i);
2741

42+
// An undefined constant must still raise the same Error PHP raises.
43+
try {
44+
var_dump(count([UNDEFINED_COUNT_LITERAL]));
45+
echo "constant-error-not-thrown\n";
46+
} catch (Error $e) {
47+
echo "caught=", $e->getMessage(), "\n";
48+
}
49+
50+
// A missing class constant on a known class must also still throw.
51+
try {
52+
var_dump(count([KnownClass::MISSING]));
53+
echo "class-constant-error-not-thrown\n";
54+
} catch (Error $e) {
55+
echo "caught=", $e->getMessage(), "\n";
56+
}
57+
58+
// An interpolated string may invoke __get(), which must still happen.
59+
$object = new MagicHolder();
60+
var_dump(count(["{$object->property}"]));
61+
62+
// A by-reference item binds the source variable instead of reading it.
63+
$ref = 1;
64+
var_dump(count([&$ref]));
65+
66+
// A defined class constant is still evaluated, not discarded.
67+
var_dump(count([KnownClass::KNOWN]));
68+
2869
// Plain literals stay eligible for the compile-time fold.
29-
$a = 1;
3070
var_dump(count([1, 2, 3]));
3171
var_dump(count([[1, 2], [3]]));
32-
var_dump(count([$a, -2, true, null]));
72+
var_dump(count([1.5, 'text', true, false, null]));
73+
var_dump(count([-2, +3, -1.5]));
3374
var_dump(count([]));
3475
}
3576
?>
@@ -41,7 +82,14 @@ int(1)
4182
int(6)
4283
int(2)
4384
int(2)
85+
caught=Undefined constant "UNDEFINED_COUNT_LITERAL"
86+
caught=Undefined constant KnownClass::MISSING
87+
get-property
88+
int(1)
89+
int(1)
90+
int(1)
4491
int(3)
4592
int(2)
46-
int(4)
93+
int(5)
94+
int(3)
4795
int(0)

0 commit comments

Comments
 (0)